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

This commit is contained in:
IndusAryan 2024-03-17 18:40:08 +05:30
commit ef6d270ac2
79 changed files with 1845 additions and 1204 deletions

View file

@ -62,7 +62,7 @@ android {
targetSdk = 33 /* Android 14 is Fu*ked
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
versionCode = 63
versionName = "4.3.1"
versionName = "4.3.2"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@ -181,7 +181,7 @@ dependencies {
// Design & UI
implementation("jp.wasabeef:glide-transformations:4.3.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.10.0")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
@ -222,6 +222,7 @@ dependencies {
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
implementation("androidx.tvprovider:tvprovider:1.0.0")
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
// Extensions & Other Libs

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="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.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Required for getting arbitrary Aniyomi packages -->

View file

@ -11,7 +11,9 @@ import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -31,7 +33,6 @@ import org.acra.sender.ReportSenderFactory
import java.io.File
import java.io.FileNotFoundException
import java.io.PrintStream
import java.lang.Exception
import java.lang.ref.WeakReference
import kotlin.concurrent.thread
import kotlin.system.exitProcess
@ -211,7 +212,7 @@ class AcraApplication : Application() {
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
isTvSettings(),
isLayout(TV or EMULATOR),
activity?.supportFragmentManager?.fragments?.lastOrNull()
)
}

View file

@ -11,11 +11,9 @@ import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.View.NO_ID
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
@ -31,11 +29,12 @@ import com.google.android.material.chip.ChipGroup
import com.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
@ -99,8 +98,7 @@ object CommonActivity {
var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
var currentToast: Toast? = null
private var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return
@ -156,25 +154,19 @@ object CommonActivity {
} catch (e: Exception) {
logError(e)
}
try {
val inflater =
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val layout: View = inflater.inflate(
R.layout.toast,
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
)
val text = layout.findViewById(R.id.text) as TextView
text.text = message.trim()
val binding = ToastBinding.inflate(act.layoutInflater)
binding.text.text = message.trim()
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
val toast = Toast(act)
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
toast.duration = duration ?: Toast.LENGTH_SHORT
toast.view = layout
//https://github.com/PureWriter/ToastCompat
toast.show()
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
toast.view = binding.root
currentToast = toast
toast.show()
} catch (e: Exception) {
logError(e)
}
@ -319,6 +311,7 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink
"Lavender" -> R.style.OverlayPrimaryColorLavender
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal

View file

@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
@ -119,7 +120,8 @@ object APIHolder {
}
fun LoadResponse.getId(): Int {
return getLoadResponseIdFromUrl(url, apiName)
// this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked
return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName)
}
/**

View file

@ -28,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.fragment.app.FragmentActivity
@ -85,6 +86,7 @@ import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
@ -111,10 +113,11 @@ import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
@ -131,11 +134,15 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
@ -166,7 +173,6 @@ import kotlin.math.absoluteValue
import kotlin.reflect.KClass
import kotlin.system.exitProcess
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
//https://wiki.videolan.org/Android_Player_Intents/
@ -285,7 +291,8 @@ var app = Requests(responseParser = object : ResponseParser {
defaultHeaders = mapOf("user-agent" to USER_AGENT)
}
class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
BiometricAuthenticator.BiometricAuthCallback {
companion object {
const val TAG = "MAINACT"
const val ANIMATED_OUTLINE: Boolean = false
@ -331,10 +338,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// kinda shitty solution, but cant com main->home otherwise for popups
val bookmarksUpdatedEvent = Event<Boolean>()
/**
* Used by DataStoreHelper to fully reload home when switching accounts
*/
val reloadHomeEvent = Event<Boolean>()
/**
* Used by DataStoreHelper to fully reload library when switching accounts
*/
@ -549,7 +558,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
binding?.navHostFragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams
val push =
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
if (!this.isLtr()) {
params.setMargins(
@ -576,7 +585,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
Configuration.ORIENTATION_PORTRAIT -> {
isTvSettings()
isLayout(TV or EMULATOR)
}
else -> {
@ -783,6 +792,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
lateinit var viewModel: ResultViewModel2
lateinit var syncViewModel: SyncViewModel
/** kinda dirty, however it signals that we should use the watch status as sync or not*/
var isLocalList: Boolean = false
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
@ -1167,11 +1177,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
binding = try {
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root)
if(isTrueTvSettings() && ANIMATED_OUTLINE) {
if (isLayout(TV) && ANIMATED_OUTLINE) {
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
@ -1183,14 +1193,30 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
newLocalBinding.focusOutline.isVisible = false
}
if(isTrueTvSettings()) {
if (isLayout(TV)) {
// Put here any button you don't want focusing it to center the view
val exceptionButtons = listOf(
R.id.home_preview_play_btt,
R.id.home_preview_info_btt,
R.id.home_preview_hidden_next_focus,
R.id.home_preview_hidden_prev_focus,
R.id.result_play_movie_button,
R.id.result_play_series_button,
R.id.result_resume_series_button,
R.id.result_play_trailer_button,
R.id.result_bookmark_Button,
R.id.result_favorite_Button,
R.id.result_subscribe_Button,
R.id.result_search_Button,
R.id.result_episodes_show_button,
)
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
centerView(newFocus)
}
}
ActivityMainBinding.bind(newLocalBinding.root) // this may crash
} else {
val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
@ -1202,7 +1228,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
null
}
changeStatusBarState(isEmulatorSettings())
changeStatusBarState(isLayout(EMULATOR))
/** 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 (isLayout(PHONE) && authEnabled && noAccounts) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
BiometricAuthenticator.promptInfo?.let { promt ->
BiometricAuthenticator.biometricPrompt?.authenticate(promt)
}
// hide background while authenticating, Sorry moms & dads 🙏
binding?.navHostFragment?.isInvisible = true
}
}
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
@ -1322,12 +1368,41 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
observe(viewModel.watchStatus) { state ->
setWatchStatus(state)
fun setSubscribeStatus(state: Boolean?) {
bottomPreviewBinding?.resultviewPreviewSubscribe?.apply {
if (state != null) {
val drawable = if (state) {
R.drawable.ic_baseline_notifications_active_24
} else {
R.drawable.baseline_notifications_none_24
}
observe(syncViewModel.userData) { status ->
setUserData(status)
setImageResource(drawable)
}
isVisible = state != null
setOnClickListener {
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleSubscriptionStatus
val message = if (newStatus) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
}
}
observe(viewModel.watchStatus,::setWatchStatus)
observe(syncViewModel.userData, ::setUserData)
observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus)
observeNullable(viewModel.page) { resource ->
if (resource == null) {
@ -1370,6 +1445,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
setUserData(syncViewModel.userData.value)
setWatchStatus(viewModel.watchStatus.value)
setSubscribeStatus(viewModel.subscribeStatus.value)
resultviewPreviewBookmark.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
@ -1388,7 +1464,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
)
}
} else {
val value = (syncViewModel.userData.value as? Resource.Success)?.value?.status ?: SyncWatchType.NONE
val value =
(syncViewModel.userData.value as? Resource.Success)?.value?.status
?: SyncWatchType.NONE
this@MainActivity.showBottomDialog(
SyncWatchType.values().map { getString(it.stringRes) }.toList(),
@ -1431,7 +1509,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
if (!isTvSettings()) // dont want this clickable on tv layout
if (isLayout(PHONE)) // dont want this clickable on tv layout
resultviewPreviewDescription.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
@ -1506,7 +1584,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
if (navDestination.matchDestination(R.id.navigation_home)) {
attachBackPressedCallback()
} else detachBackPressedCallback()
@ -1542,7 +1620,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
itemRippleColor = rippleColor
itemActiveIndicatorColor = rippleColor
setupWithNavController(navController)
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
background?.alpha = 200
} else {
background?.alpha = 255
@ -1743,6 +1821,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
)
}
/** Biometric stuff **/
override fun onAuthenticationSuccess() {
// make background (nav host fragment) visible again
binding?.navHostFragment?.isInvisible = false
}
private var backPressedCallback: OnBackPressedCallback? = null
private fun attachBackPressedCallback() {

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) {
liveData.removeObservers(this)
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) {
liveData.removeObservers(this)
liveData.observe(this) { action(it) }
}

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.services
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
@ -12,7 +13,7 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
@ -97,7 +98,9 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
)
}
@SuppressLint("UnspecifiedImmutableFlag")
override suspend fun doWork(): Result {
try {
// println("Update subscriptions!")
context.createNotificationChannel(
SUBSCRIPTION_CHANNEL_ID,
@ -215,10 +218,18 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
// You can probably get some issues here since this is async but it does not matter much.
updateProgress(max, ++progress, false)
} catch (_: Throwable) {
} catch (t: Throwable) {
logError(t)
}
}
return Result.success()
} catch (t: Throwable) {
logError(t)
// ye, while this is not correct, but because gods know why android just crashes
// and this causes major battery usage as it retries it inf times. This is better, just
// in case android decides to be android and fuck us
return Result.success()
}
}
}

View file

@ -8,7 +8,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
@ -71,9 +72,9 @@ class LocalList : SyncAPI {
}?.distinctBy { it.first } ?: return null
val list = ioWork {
val isTrueTv = isTrueTvSettings()
val isTrueTv = isLayout(TV)
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
val baseMap = WatchType.entries.filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(

View file

@ -440,9 +440,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
interceptor = interceptor
).isSuccessful
} else {
val statusResponse = status?.let { setStatus ->
val statusResponse = this.status?.let { setStatus ->
val newStatus =
SimklListStatusType.values()
SimklListStatusType.entries
.firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!!
@ -479,9 +479,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
).isSuccessful
} ?: true
// You cannot rate if you are planning to watch it.
val shouldRate =
score != null && status != SimklListStatusType.Planning.value
val realScore = if (shouldRate) score else null
val historyResponse =
// Only post if there are episodes or score to upload
if (addEpisodes != null || score != null) {
if (addEpisodes != null || shouldRate) {
app.post(
"${this.url}/sync/history",
json = StatusRequest(
@ -492,8 +497,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
ids,
addEpisodes?.first,
addEpisodes?.second,
score,
score?.let { time },
realScore,
realScore?.let { time },
)
), movies = emptyList()
),
@ -827,7 +832,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
if (foundItem != null) {
return SimklSyncStatus(
status = foundItem.status?.let { SyncWatchType.fromInternalId(SimklListStatusType.fromString(it)?.value) }
status = foundItem.status?.let {
SyncWatchType.fromInternalId(
SimklListStatusType.fromString(
it
)?.value
)
}
?: return null,
score = foundItem.user_rating,
watchedEpisodes = foundItem.watched_episodes_count,
@ -859,8 +870,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val builder = SimklScoreBuilder.Builder()
.apiUrl(this.mainUrl)
.score(status.score, simklStatus?.oldScore)
.status(status.status.internalId, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
SimklListStatusType.values().firstOrNull {
.status(
status.status.internalId,
(status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
SimklListStatusType.entries.firstOrNull {
it.originalName == oldStatus
}?.value
})
@ -996,7 +1009,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val list = getSyncListSmart() ?: return null
val baseMap =
SimklListStatusType.values()
SimklListStatusType.entries
.filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value }
.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>()

View file

@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.databinding.AccountListItemBinding
import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -38,7 +40,7 @@ class AccountAdapter(
is AccountListItemBinding -> binding.apply {
if (account == null) return@apply
val isTv = isTvSettings() || !root.isInTouchMode
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
@ -80,7 +82,7 @@ class AccountAdapter(
is AccountListItemEditBinding -> binding.apply {
if (account == null) return@apply
val isTv = isTvSettings() || !root.isInTouchMode
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.account
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
@ -17,13 +18,19 @@ import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
class AccountSelectActivity : AppCompatActivity() {
class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback {
lateinit var viewModel: AccountViewModel
@ -41,13 +48,36 @@ class AccountSelectActivity : AppCompatActivity() {
)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val skipStartup = settingsManager.getBoolean(
getString(R.string.skip_startup_account_select_key),
false
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false)
val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false
) || accounts.count() <= 1
viewModel = ViewModelProvider(this)[AccountViewModel::class.java]
fun askBiometricAuth() {
if (isLayout(PHONE) && authEnabled) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(
this,
R.string.biometric_authentication_title,
false
)
BiometricAuthenticator.promptInfo?.let { promt ->
BiometricAuthenticator.biometricPrompt?.authenticate(promt)
}
}
}
}
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) {
// We are allowed to continue to MainActivity
navigateToMainActivity()
}
}
// Don't show account selection if there is only
// one account that exists
if (!isEditingFromMainActivity && skipStartup) {
@ -55,12 +85,6 @@ class AccountSelectActivity : AppCompatActivity() {
if (currentAccount?.lockPin != null) {
CommonActivity.init(this)
viewModel.handleAccountSelect(currentAccount, this, true)
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) {
// We are allowed to continue to MainActivity
navigateToMainActivity()
}
}
} else {
if (accounts.count() > 1) {
showToast(this, getString(
@ -88,12 +112,6 @@ class AccountSelectActivity : AppCompatActivity() {
// Handle the selected account
accountSelectCallback = {
viewModel.handleAccountSelect(it, this)
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) {
// We are allowed to continue to MainActivity
navigateToMainActivity()
}
}
},
accountCreateCallback = { viewModel.handleAccountUpdate(it, this) },
accountEditCallback = {
@ -111,7 +129,7 @@ class AccountSelectActivity : AppCompatActivity() {
recyclerView.adapter = adapter
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
binding.editAccountButton.setBackgroundResource(
R.drawable.player_button_tv_attr_no_bg
)
@ -152,12 +170,14 @@ class AccountSelectActivity : AppCompatActivity() {
viewModel.toggleIsEditing()
}
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) {
liveAccounts.count() + 1
} else 6
}
}
askBiometricAuth()
}
private fun navigateToMainActivity() {
@ -165,4 +185,8 @@ class AccountSelectActivity : AppCompatActivity() {
startActivity(mainIntent)
finish() // Finish the account selection activity
}
override fun onAuthenticationSuccess() {
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
}
}

View file

@ -5,6 +5,7 @@ import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -13,17 +14,25 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
import com.lagradost.cloudstream3.databinding.StreamInputBinding
import com.lagradost.cloudstream3.isMovieType
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LinkGenerator
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
@ -34,15 +43,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import android.text.format.Formatter.formatShortFileSize
import androidx.core.widget.doOnTextChanged
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
import com.lagradost.cloudstream3.databinding.StreamInputBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import java.net.URI
@ -200,7 +200,7 @@ class DownloadFragment : Fragment() {
}
// Should be visible in emulator layout
binding?.downloadStreamButton?.isGone = isTrueTvSettings()
binding?.downloadStreamButton?.isGone = isLayout(TV)
binding?.downloadStreamButton?.setOnClickListener {
val dialog =
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)

View file

@ -2,16 +2,19 @@ package com.lagradost.cloudstream3.ui.download.button
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Looper
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.animation.AnimationUtils
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
@ -241,14 +244,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
}
}*/
/** Also sets currentStatus */
override fun setStatus(status: DownloadStatusTell?) {
currentStatus = status
//progressBar.isVisible =
// status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error
//progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete
progressBarBackground.post {
@MainThread
private fun setStatusInternal(status : DownloadStatusTell?) {
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
@ -276,6 +273,26 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
progressBarBackground.isGone = hide
progressBar.isGone = hide
}
/** Also sets currentStatus */
override fun setStatus(status: DownloadStatusTell?) {
currentStatus = status
// runs on the main thread, but also instant if it already is
if (Looper.myLooper() == Looper.getMainLooper()) {
try {
setStatusInternal(status)
} catch (t : Throwable) {
logError(t) // just in case setStatusInternal throws because thread
progressBarBackground.post {
setStatusInternal(status)
}
}
} else {
progressBarBackground.post {
setStatusInternal(status)
}
}
}
override fun resetView() {

View file

@ -42,8 +42,10 @@ import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLine
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
@ -311,7 +313,7 @@ class HomeFragment : Fragment() {
button?.isVisible = isValid
button?.isChecked = isValid && selectedTypes.any { types.contains(it) }
button?.isFocusable = true
if (isTrueTvSettings()) {
if (isLayout(TV)) {
button?.isFocusableInTouchMode = true
}
@ -435,7 +437,7 @@ class HomeFragment : Fragment() {
bottomSheetDialog?.ownShow()
val layout =
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home
if (isLayout(TV or EMULATOR)) R.layout.fragment_home_tv else R.layout.fragment_home
val root = inflater.inflate(layout, container, false)
binding = try {
FragmentHomeBinding.bind(root)
@ -449,6 +451,11 @@ class HomeFragment : Fragment() {
}
override fun onDestroyView() {
homeMasterAdapter?.onSaveInstanceState(
instanceState,
binding?.homeMasterRecycler
)
bottomSheetDialog?.ownHide()
binding = null
super.onDestroyView()
@ -485,6 +492,10 @@ class HomeFragment : Fragment() {
private var bottomSheetDialog: BottomSheetDialog? = null
// https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32
// cry about it, but this is android we are talking about, we cant do the most simple shit without making a global variable
private var instanceState: Bundle = Bundle()
private var homeMasterAdapter: HomeParentItemAdapterPreview? = null
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -505,15 +516,16 @@ class HomeFragment : Fragment() {
activity.loadSearchResult(listHomepageItems.random())
}
}
homeMasterRecycler.adapter =
HomeParentItemAdapterPreview(
homeMasterAdapter = HomeParentItemAdapterPreview(
mutableListOf(),
homeViewModel
)
homeViewModel,
).apply {
onRestoreInstanceState(instanceState)
}
homeMasterRecycler.adapter = homeMasterAdapter
//fixPaddingStatusbar(homeLoadingStatusbar)
homeApiFab.isVisible = !isTvSettings()
homeApiFab.isVisible = isLayout(PHONE)
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@ -521,7 +533,7 @@ class HomeFragment : Fragment() {
homeApiFab.shrink() // hide
homeRandom.shrink()
} else if (dy < -5) {
if (!isTvSettings()) {
if (isLayout(PHONE)) {
homeApiFab.extend() // show
homeRandom.extend()
}
@ -529,6 +541,7 @@ class HomeFragment : Fragment() {
super.onScrolled(recyclerView, dx, dy)
}
})
}
@ -539,7 +552,7 @@ class HomeFragment : Fragment() {
settingsManager.getBoolean(
getString(R.string.random_button_key),
false
) && !isTvSettings()
) && isLayout(PHONE)
binding?.homeRandom?.visibility = View.GONE
}

View file

@ -1,5 +1,7 @@
package com.lagradost.cloudstream3.ui.home
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -7,16 +9,20 @@ import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
class LoadClickCallback(
@ -32,18 +38,90 @@ open class ParentItemAdapter(
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
) : RecyclerView.Adapter<ViewHolder>() {
// Ok, this is fucked, but there is a reason for this as we want to resume 1. when scrolling up and down
// and 2. when doing into a thing and coming back. 1 is always active, but 2 requires doing it in the fragment
// as OnCreateView is called and this adapter is recreated losing the internal state to the GC
//
// 1. This works by having the adapter having a internal state "scrollStates" that keeps track of the states
// when a view recycles, it looks up this internal state
// 2. To solve the the coming back shit we have to save "scrollStates" to a Bundle inside the
// fragment via onSaveInstanceState, because this cant be easy for some reason as the adapter does
// not have a state but the layout-manager for no reason, then it is resumed via onRestoreInstanceState
//
// Even when looking at a real example they do this :skull:
// https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32
private val scrollStates = mutableMapOf<Int, Parcelable?>()
companion object {
private const val SCROLL_KEY: String = "ParentItemAdapter::scrollStates.keys"
private const val SCROLL_VALUE: String = "ParentItemAdapter::scrollStates.values"
}
open fun onRestoreInstanceState(savedInstanceState: Bundle?) {
try {
val keys = savedInstanceState?.getIntArray(SCROLL_KEY) ?: intArrayOf()
val values = savedInstanceState?.getParcelableArray(SCROLL_VALUE) ?: arrayOf()
for ((k, v) in keys.zip(values)) {
this.scrollStates[k] = v
}
} catch (t: Throwable) {
logError(t)
}
}
open fun onSaveInstanceState(outState: Bundle, recyclerView: RecyclerView? = null) {
if (recyclerView != null) {
for (position in items.indices) {
val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: continue
saveHolder(holder)
}
}
outState.putIntArray(SCROLL_KEY, scrollStates.keys.toIntArray())
outState.putParcelableArray(SCROLL_VALUE, scrollStates.values.toTypedArray())
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is ParentViewHolder -> {
holder.bind(items[position])
scrollStates[holder.absoluteAdapterPosition]?.let {
holder.binding.homeChildRecyclerview.layoutManager?.onRestoreInstanceState(it)
}
}
}
}
private fun saveHolder(holder : ViewHolder) {
when (holder) {
is ParentViewHolder -> {
scrollStates[holder.absoluteAdapterPosition] =
holder.binding.homeChildRecyclerview.layoutManager?.onSaveInstanceState()
}
}
}
override fun onViewRecycled(holder: ViewHolder) {
saveHolder(holder)
super.onViewRecycled(holder)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutResId = when {
isTrueTvSettings() -> R.layout.homepage_parent_tv
parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator
isLayout(TV) -> R.layout.homepage_parent_tv
isLayout(EMULATOR) -> R.layout.homepage_parent_emulator
else -> R.layout.homepage_parent
}
val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
val binding = HomepageParentBinding.bind(root)
val inflater = LayoutInflater.from(parent.context)
val binding = try {
HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false))
} catch (t : Throwable) {
logError(t)
// just in case someone forgot we don't want to crash
HomepageParentBinding.inflate(inflater)
}
return ParentViewHolder(
binding,
@ -53,14 +131,6 @@ open class ParentItemAdapter(
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ParentViewHolder -> {
holder.bind(items[position])
}
}
}
override fun getItemCount(): Int {
return items.size
}
@ -116,7 +186,6 @@ open class ParentItemAdapter(
}
override fun onChanged(_position: Int, count: Int, payload: Any?) {
val position = _position + delta
// I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind
@ -155,15 +224,15 @@ open class ParentItemAdapter(
//diffResult.dispatchUpdatesTo(this)
}
class ParentViewHolder
constructor(
class ParentViewHolder(
val binding: HomepageParentBinding,
// val viewModel: HomeViewModel,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null,
) :
RecyclerView.ViewHolder(binding.root) {
ViewHolder(binding.root) {
val title: TextView = binding.homeChildMoreInfo
private val recyclerView: RecyclerView = binding.homeChildRecyclerview
private val startFocus = R.id.nav_rail_view
@ -237,7 +306,7 @@ open class ParentItemAdapter(
})
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
if (!isTrueTvSettings()) {
if (isLayout(PHONE)) {
title.setOnClickListener {
moreInfoClickCallback.invoke(expand)
}

View file

@ -36,8 +36,9 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
@ -48,7 +49,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.populateChips
class HomeParentItemAdapterPreview(
items: MutableList<HomeViewModel.ExpandableHomepageList>,
private val viewModel: HomeViewModel,
) : ParentItemAdapter(items, clickCallback = {
) : ParentItemAdapter(items,
clickCallback = {
viewModel.click(it)
}, moreInfoClickCallback = {
viewModel.popup(it)
@ -78,13 +80,13 @@ class HomeParentItemAdapterPreview(
return when (viewType) {
VIEW_TYPE_HEADER -> {
val inflater = LayoutInflater.from(parent.context)
val binding = if (isTvSettings()) FragmentHomeHeadTvBinding.inflate(
val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate(
inflater,
parent,
false
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) {
if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) {
binding.homeBookmarkParentItemMoreInfo.isVisible = true
val marginInDp = 50
@ -598,7 +600,7 @@ class HomeParentItemAdapterPreview(
if (
binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings()
isLayout(EMULATOR)
) {
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
@ -628,7 +630,7 @@ class HomeParentItemAdapterPreview(
if (
binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings()
isLayout(EMULATOR)
) {
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle

View file

@ -10,7 +10,9 @@ import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper.setImage
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -40,7 +42,7 @@ class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = if (isTvSettings()) {
val binding = if (isLayout(TV or EMULATOR)) {
HomeScrollViewTvBinding.inflate(inflater, parent, false)
} else {
HomeScrollViewBinding.inflate(inflater, parent, false)

View file

@ -34,7 +34,8 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -132,7 +133,7 @@ class HomeViewModel : ViewModel() {
private fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatchingResult = getResumeWatching()
if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isLayout(TV) && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ioSafe {
// this WILL crash on non tvs, so keep this inside a try catch
activity?.addProgramsToContinueWatching(resumeWatchingResult)

View file

@ -49,6 +49,11 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
@ -101,7 +106,7 @@ class LibraryFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
val layout =
if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library
if (isLayout(TV or EMULATOR)) R.layout.fragment_library_tv else R.layout.fragment_library
val root = inflater.inflate(layout, container, false)
binding = try {
FragmentLibraryBinding.bind(root)
@ -131,6 +136,18 @@ class LibraryFragment : Fragment() {
super.onSaveInstanceState(outState)
}
private fun updateRandom() {
val position = libraryViewModel.currentPage.value ?: 0
val pages = (libraryViewModel.pages.value as? Resource.Success)?.value ?: return
if (toggleRandomButton) {
listLibraryItems.clear()
listLibraryItems.addAll(pages[position].items)
binding?.libraryRandom?.isVisible = listLibraryItems.isNotEmpty()
} else {
binding?.libraryRandom?.isGone = true
}
}
@SuppressLint("ResourceType", "CutPasteId")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -208,7 +225,7 @@ class LibraryFragment : Fragment() {
settingsManager.getBoolean(
getString(R.string.random_button_key),
false
) && !SettingsFragment.isTvSettings()
) && isLayout(PHONE)
binding?.libraryRandom?.visibility = View.GONE
}
@ -395,15 +412,7 @@ class LibraryFragment : Fragment() {
binding?.viewpager?.setCurrentItem(page, false)
}
observe(libraryViewModel.currentPage){
if (toggleRandomButton) {
listLibraryItems.clear()
listLibraryItems.addAll(pages[it].items)
libraryRandom.isVisible = listLibraryItems.isNotEmpty()
} else {
libraryRandom.isGone = true
}
}
updateRandom()
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect:
@ -481,6 +490,7 @@ class LibraryFragment : Fragment() {
}
observe(libraryViewModel.currentPage) { position ->
updateRandom()
val all = binding?.viewpager?.allViews?.toList()
?.filterIsInstance<AutofitRecyclerView>()

View file

@ -1,7 +1,6 @@
package com.lagradost.cloudstream3.ui.library
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.doOnAttach
@ -12,7 +11,9 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
class ViewpagerAdapter(
@ -73,7 +74,7 @@ class ViewpagerAdapter(
val diff = scrollY - oldScrollY
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior
if (SettingsFragment.isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
.apply {
if (diff <= 0)

View file

@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.DataStoreHelper
@ -1514,7 +1515,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
}
// cs3 is peak media center
setRemainingTimeCounter(durationMode || SettingsFragment.isTrueTvSettings())
setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV))
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
updateRemainingTime()
}

View file

@ -39,7 +39,10 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
import com.lagradost.cloudstream3.ui.result.*
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.utils.*
@ -1275,8 +1278,7 @@ class GeneratorPlayer : FullScreenPlayer() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
// this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason
isTv = isTvSettings()
layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
sync = ViewModelProvider(this)[SyncViewModel::class.java]

View file

@ -9,6 +9,9 @@ import android.util.Log
import androidx.annotation.WorkerThread
import androidx.core.graphics.scale
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -63,7 +66,7 @@ interface IPreviewGenerator {
companion object {
fun new(): IPreviewGenerator {
/** because TV has low ram + not show we disable this for now */
return if (SettingsFragment.isTrueTvSettings()) {
return if (isLayout(TV)) {
empty()
} else {
PreviewGenerator()

View file

@ -34,7 +34,8 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.search.SearchViewModel
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -277,7 +278,7 @@ class QuickSearchFragment : Fragment() {
activity?.popCurrentPage()
}
if (isTrueTvSettings()) {
if (isLayout(TV)) {
binding?.quickSearch?.requestFocus()
}

View file

@ -15,8 +15,10 @@ import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
@ -172,15 +174,13 @@ class EpisodeAdapter(
@SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) {
localCard = card
val setWidth =
if (isTvSettings()) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT
if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT
binding.episodeLinHolder.layoutParams.width = setWidth
binding.episodeHolderLarge.layoutParams.width = setWidth
binding.episodeHolder.layoutParams.width = setWidth
val isTrueTv = isTrueTvSettings()
binding.apply {
downloadButton.isVisible = hasDownloadSupport
@ -246,12 +246,21 @@ class EpisodeAdapter(
episodeDescript.apply {
text = card.description.html()
isGone = text.isNullOrBlank()
var isExpanded = false
setOnClickListener {
if (isLayout(TV)) {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
} else {
isExpanded = !isExpanded
maxLines = if (isExpanded) {
Integer.MAX_VALUE
} else 4
}
}
}
if (!isTrueTv) {
if (isLayout(EMULATOR or PHONE)) {
episodePoster.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
@ -266,7 +275,7 @@ class EpisodeAdapter(
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
if (isTrueTv) {
if (isLayout(TV)) {
itemView.isFocusable = true
itemView.isFocusableInTouchMode = true
//itemView.touchscreenBlocksFocus = false
@ -291,11 +300,9 @@ class EpisodeAdapter(
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) {
val isTrueTv = isTrueTvSettings()
binding.episodeHolder.layoutParams.apply {
width =
if (isTvSettings()) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT
if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT
}
binding.apply {
@ -352,7 +359,7 @@ class EpisodeAdapter(
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
if (isTrueTv) {
if (isLayout(TV)) {
itemView.isFocusable = true
itemView.isFocusableInTouchMode = true
//itemView.touchscreenBlocksFocus = false

View file

@ -5,7 +5,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
/*
class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter<Int>(context, resource) {
@ -83,7 +84,7 @@ class ImageAdapter(
this.nextFocusUpId = nextFocusUp
}
if (clickCallback != null) {
if (isTrueTvSettings()) {
if (isLayout(TV)) {
isClickable = true
isLongClickable = true
isFocusable = true

View file

@ -2,9 +2,6 @@ package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Rect
@ -34,7 +31,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
@ -62,16 +58,15 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
@ -688,14 +683,15 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.setImage(d.posterImage)
resultPosterBackground.setImage(d.posterBackgroundImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener {
activity?.let { activity ->
activity.showBottomDialogText(
d.titleText.asString(activity),
d.plotText.asString(activity).html(),
{}
)
var isExpanded = false
resultDescription.apply {
setTextHtml(d.plotText)
setOnClickListener {
isExpanded = !isExpanded
maxLines = if (isExpanded) {
Integer.MAX_VALUE
} else 10
}
}
@ -758,14 +754,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure
resultTitle.setOnLongClickListener {
val titleToCopy = resultTitle.text
val clipboardManager =
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Title", titleToCopy))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
showToast(R.string.copyTitle, Toast.LENGTH_SHORT)
}
return@setOnLongClickListener true
clipboardHelper(txt(R.string.title), resultTitle.text)
true
}
}
}
@ -901,14 +891,6 @@ open class ResultFragmentPhone : FullScreenPlayer() {
observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null)
}
observe(viewModel.episodeSynopsis) { description ->
activity?.let { activity ->
activity.showBottomDialogText(
activity.getString(R.string.synopsis),
description.html()
) { viewModel.releaseEpisodeSynopsis() }
}
}
context?.let { ctx ->
val arrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
/*

View file

@ -33,13 +33,16 @@ import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData
import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
@ -129,9 +132,9 @@ class ResultFragmentTv : Fragment() {
* Note that this will steal any focus if the episode loading is too slow (unlikely).
*/
private fun focusPlayButton() {
binding?.resultPlayMovie?.requestFocus()
binding?.resultPlaySeries?.requestFocus()
binding?.resultResumeSeries?.requestFocus()
binding?.resultPlayMovieButton?.requestFocus()
binding?.resultPlaySeriesButton?.requestFocus()
binding?.resultResumeSeriesButton?.requestFocus()
}
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
@ -246,37 +249,15 @@ class ResultFragmentTv : Fragment() {
storedData.start
)
// ===== ===== =====
var comingSoon = false
binding?.apply {
//episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f
val leftListener: View.OnFocusChangeListener =
View.OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@OnFocusChangeListener
toggleEpisodes(false)
}
val rightListener: View.OnFocusChangeListener =
View.OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@OnFocusChangeListener
toggleEpisodes(true)
}
resultPlayMovie.onFocusChangeListener = leftListener
resultPlaySeries.onFocusChangeListener = leftListener
resultResumeSeries.onFocusChangeListener = leftListener
resultPlayTrailer.onFocusChangeListener = leftListener
resultEpisodesShow.onFocusChangeListener = rightListener
resultDescription.onFocusChangeListener = leftListener
resultBookmarkButton.onFocusChangeListener = leftListener
resultFavoriteButton.onFocusChangeListener = leftListener
resultEpisodesShow.setOnClickListener {
// toggle, to make it more touch accessable just in case someone thinks that a
// tv layout is better but is using a touch device
toggleEpisodes(!episodeHolderTv.isVisible)
}
// resultEpisodes.onFocusChangeListener = leftListener
// parallax on background
resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { view, _, scrollY, _, oldScrollY ->
backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f
})
redirectToPlay.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@setOnFocusChangeListener
@ -284,13 +265,14 @@ class ResultFragmentTv : Fragment() {
binding?.apply {
val views = listOf(
resultPlayMovie,
resultPlaySeries,
resultResumeSeries,
resultPlayTrailer,
resultPlayMovieButton,
resultPlaySeriesButton,
resultResumeSeriesButton,
resultPlayTrailerButton,
resultBookmarkButton,
resultFavoriteButton,
resultSubscribeButton
resultSubscribeButton,
resultSearchButton
)
for (requestView in views) {
if (!requestView.isVisible) continue
@ -299,11 +281,6 @@ class ResultFragmentTv : Fragment() {
}
}
// parallax on background
resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f
})
redirectToEpisodes.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@setOnFocusChangeListener
toggleEpisodes(true)
@ -313,7 +290,7 @@ class ResultFragmentTv : Fragment() {
resultSeasonSelection,
resultRangeSelection,
resultEpisodes,
resultPlayTrailer,
resultPlayTrailerButton,
)
for (requestView in views) {
if (!requestView.isShown) continue
@ -322,6 +299,46 @@ 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 { view, hasFocus ->
if (!hasFocus) {
text.isSelected = false
if (view.id == R.id.result_episodes_show_button) toggleEpisodes(false)
return@setOnFocusChangeListener
}
text.isSelected = true
if (button.tag == context?.getString(R.string.tv_no_focus_tag)){
resultFinishLoading.scrollTo(0,0)
}
when (button.id) {
R.id.result_episodes_show_button -> {
toggleEpisodes(true)
}
else -> {
toggleEpisodes(false)
}
}
}
}
resultEpisodesShowButton.setOnClickListener {
// toggle, to make it more touch accessible just in case someone thinks that a
// tv layout is better but is using a touch device
toggleEpisodes(!episodeHolderTv.isVisible)
}
resultEpisodes.setLinearListLayout(
isHorizontal = false,
nextUp = FOCUS_SELF,
@ -363,10 +380,6 @@ class ResultFragmentTv : Fragment() {
resultMetaSite.isFocusable = false
//resultReloadConnectionOpenInBrowser.setOnClickListener {view ->
// view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true)
//}
resultSeasonSelection.setAdapter()
resultRangeSelection.setAdapter()
resultDubSelection.setAdapter()
@ -430,9 +443,9 @@ class ResultFragmentTv : Fragment() {
val aboveCast = listOf(
binding?.resultEpisodesShow,
binding?.resultBookmarkButton,
binding?.resultFavoriteButton,
binding?.resultSubscribeButton,
binding?.resultBookmark,
binding?.resultFavorite,
binding?.resultSubscribe,
).firstOrNull {
it?.isVisible == true
}
@ -443,8 +456,16 @@ class ResultFragmentTv : Fragment() {
observeNullable(viewModel.resumeWatching) { resume ->
binding?.apply {
if (resume == null) {
return@observeNullable
}
resultResumeSeries.isVisible = true
resultPlayMovie.isVisible = false
resultPlaySeries.isVisible = false
// show progress no matter if series or movie
resume?.progress?.let { progress ->
resume.progress?.let { progress ->
resultResumeSeriesProgressText.setText(progress.progressLeft)
resultResumeSeriesProgress.apply {
isVisible = true
@ -456,37 +477,20 @@ class ResultFragmentTv : Fragment() {
resultResumeProgressHolder.isVisible = false
}
// if movie then hide both as movie button is
// always visible on movies, this is done in movie observe
if (resume?.isMovie == true) {
resultPlaySeries.isVisible = false
resultResumeSeries.isVisible = false
return@observeNullable
}
// if series then
// > resultPlaySeries is visible when null
// > resultResumeSeries is visible when not null
if (resume == null) {
resultPlaySeries.isVisible = true
resultResumeSeries.isVisible = false
return@observeNullable
}
resultPlaySeries.isVisible = false
resultResumeSeries.isVisible = true
focusPlayButton()
// Stops last button right focus if it is a movie
if (resume.isMovie)
resultSearchButton.nextFocusRightId = R.id.result_search_Button
resultResumeSeries.text =
if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull(
null, // resume.result.name, we don't want episode title
resume.result.episode,
resume.result.season
)
resultResumeSeriesText.text =
when {
resume.isMovie -> context?.getString(R.string.resume)
resume.result.season != null ->
"${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}"
else -> "${getString(R.string.episode)} ${resume.result.episode}"
}
resultResumeSeries.setOnClickListener {
resultResumeSeriesButton.setOnClickListener {
viewModel.handleAction(
EpisodeClickEvent(
storedData.playerAction, //?: ACTION_PLAY_EPISODE_IN_PLAYER,
@ -495,7 +499,7 @@ class ResultFragmentTv : Fragment() {
)
}
resultResumeSeries.setOnLongClickListener {
resultResumeSeriesButton.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result)
)
@ -509,9 +513,9 @@ class ResultFragmentTv : Fragment() {
context?.updateHasTrailers()
if (!LoadResponse.isTrailersEnabled) return@observe
val trailers = trailersLinks.flatMap { it.mirros }
binding?.resultPlayTrailer?.apply {
isGone = trailers.isEmpty()
setOnClickListener {
binding?.apply {
resultPlayTrailer.isGone = trailers.isEmpty()
resultPlayTrailerButton.setOnClickListener {
if (trailers.isEmpty()) return@setOnClickListener
activity.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
@ -526,24 +530,38 @@ class ResultFragmentTv : Fragment() {
}
observe(viewModel.watchStatus) { watchType ->
binding?.resultBookmarkButton?.apply {
setText(watchType.stringRes)
binding?.apply {
resultBookmarkText.setText(watchType.stringRes)
resultBookmarkButton.apply {
val drawable = if (watchType.stringRes == R.string.type_none) {
R.drawable.outline_bookmark_add_24
} else {
R.drawable.ic_baseline_bookmark_24
}
setIconResource(drawable)
setOnClickListener { view ->
activity?.showBottomDialog(
WatchType.values().map { view.context.getString(it.stringRes) }.toList(),
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.values()[it], context)
viewModel.updateWatchStatus(WatchType.entries[it], context)
}
}
}
}
}
observeNullable(viewModel.favoriteStatus) { isFavorite ->
binding?.resultFavorite?.isVisible = isFavorite != null
binding?.resultFavoriteButton?.apply {
isVisible = isFavorite != null
if (isFavorite == null) return@observeNullable
val drawable = if (isFavorite) {
@ -552,14 +570,8 @@ class ResultFragmentTv : Fragment() {
R.drawable.ic_baseline_favorite_border_24
}
val text = if (isFavorite) {
R.string.action_remove_from_favorites
} else {
R.string.action_add_to_favorites
}
setIconResource(drawable)
setText(text)
setOnClickListener {
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus
@ -576,11 +588,21 @@ class ResultFragmentTv : Fragment() {
}
}
}
binding?.resultFavoriteText?.apply {
val text = if (isFavorite == true) {
R.string.unfavorite
} else {
R.string.favorite
}
setText(text)
}
}
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
binding?.resultSubscribe?.isVisible = isSubscribed != null && isLayout(EMULATOR)
binding?.resultSubscribeButton?.apply {
isVisible = isSubscribed != null && context.isEmulatorSettings()
if (isSubscribed == null) return@observeNullable
val drawable = if (isSubscribed) {
@ -589,14 +611,8 @@ class ResultFragmentTv : Fragment() {
R.drawable.baseline_notifications_none_24
}
val text = if (isSubscribed) {
R.string.action_unsubscribe
} else {
R.string.action_subscribe
}
setIconResource(drawable)
setText(text)
setOnClickListener {
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleSubscriptionStatus
@ -614,30 +630,47 @@ class ResultFragmentTv : Fragment() {
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
binding?.resultSubscribeText?.apply {
val text = if (isSubscribed) {
R.string.action_unsubscribe
} else {
R.string.action_subscribe
}
setText(text)
}
}
}
observeNullable(viewModel.movie) { data ->
if (data == null ) {
return@observeNullable
}
binding?.apply {
resultPlayMovie.isVisible = data is Resource.Success
resultPlaySeries.isVisible = data == null
seriesHolder.isVisible = data == null
resultEpisodesShow.isVisible = data == null
(data as? Resource.Success)?.value?.let { (text, ep) ->
resultPlayMovie.setText(text)
resultPlayMovie.setOnClickListener {
resultPlayMovieButton.setOnClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
)
}
resultPlayMovie.setOnLongClickListener {
resultPlayMovieButton.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep)
)
return@setOnLongClickListener true
}
focusPlayButton()
resultPlayMovie.isVisible = !comingSoon && resultResumeSeries.isGone
if (comingSoon)
resultBookmarkButton.requestFocus()
else
resultPlayMovieButton.requestFocus()
// Stops last button right focus
resultSearchButton.nextFocusRightId = R.id.result_search_Button
}
}
}
@ -720,6 +753,8 @@ class ResultFragmentTv : Fragment() {
observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null)
}
if (isLayout(TV)) {
observe(viewModel.episodeSynopsis) { description ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
@ -732,23 +767,29 @@ class ResultFragmentTv : Fragment() {
.show()
}
}
}
// Used to request focus the first time the episodes are loaded.
var hasLoadedEpisodesOnce = false
observeNullable(viewModel.episodes) { episodes ->
if (episodes == null) return@observeNullable
binding?.apply {
resultEpisodes.isVisible = episodes is Resource.Success
if (comingSoon)
resultBookmarkButton.requestFocus()
// resultEpisodeLoading.isVisible = episodes is Resource.Loading
if (episodes is Resource.Success) {
val first = episodes.value.firstOrNull()
if (first != null) {
resultPlaySeries.text = context?.getNameFull(
null, // resume.result.name, we don't want episode title
first.episode,
first.season
)
resultPlaySeries.setOnClickListener {
resultPlaySeriesText.text =
when {
first.season != null ->
"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
else -> "${getString(R.string.episode)} ${first.episode}"
}
resultPlaySeriesButton.setOnClickListener {
viewModel.handleAction(
EpisodeClickEvent(
ACTION_CLICK_DEFAULT,
@ -756,7 +797,7 @@ class ResultFragmentTv : Fragment() {
)
)
}
resultPlaySeries.setOnLongClickListener {
resultPlaySeriesButton.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, first)
)
@ -764,7 +805,9 @@ class ResultFragmentTv : Fragment() {
}
if (!hasLoadedEpisodesOnce) {
hasLoadedEpisodesOnce = true
focusPlayButton()
resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon
resultEpisodesShow.isVisible = true && !comingSoon
resultPlaySeriesButton.requestFocus()
}
}
@ -826,13 +869,23 @@ class ResultFragmentTv : Fragment() {
resultMetaYear.setText(d.yearText)
resultMetaDuration.setText(d.durationText)
resultMetaRating.setText(d.ratingText)
resultMetaStatus.setText(d.onGoingText)
resultMetaContentRating.setText(d.contentRatingText)
resultCastText.setText(d.actorsText)
resultNextAiring.setText(d.nextAiringEpisode)
resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.setImage(d.posterImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view ->
var isExpanded = false
resultDescription.apply {
setTextHtml(d.plotText)
setOnClickListener {
if (isLayout(EMULATOR)) {
isExpanded = !isExpanded
maxLines = if (isExpanded) {
Integer.MAX_VALUE
} else 10
} else {
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
@ -841,6 +894,8 @@ class ResultFragmentTv : Fragment() {
.show()
}
}
}
}
val error = listOf(
R.drawable.profile_bg_dark_blue,
@ -859,8 +914,9 @@ class ResultFragmentTv : Fragment() {
radius = 0,
errorImageDrawable = error
)
resultComingSoon.isVisible = d.comingSoon
resultDataHolder.isGone = d.comingSoon
comingSoon = d.comingSoon
resultTvComingSoon.isVisible = d.comingSoon
UIHelper.populateChips(resultTag, d.tags)
resultCastItems.isGone = d.actors.isNullOrEmpty()
(resultCastItems.adapter as? ActorAdaptor)?.updateList(
@ -871,6 +927,10 @@ class ResultFragmentTv : Fragment() {
// If there is no rating to display, we don't want an empty gap
resultMetaContentRating.width = 0
}
resultSearchButton.setOnClickListener {
QuickSearchFragment.pushSearch(activity, d.title)
}
}
is Resource.Loading -> {

View file

@ -20,6 +20,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.getCastSession
@ -31,6 +32,7 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
@ -79,12 +81,12 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import kotlinx.coroutines.*
import java.io.File
import java.util.concurrent.TimeUnit
/** This starts at 1 */
data class EpisodeRange(
// used to index data
@ -261,8 +263,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
metaText =
if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null,
durationText = if (dur == null || dur <= 0) null else txt(
R.string.duration_format,
dur
secondsToReadable(dur * 60, "0 mins")
),
onGoingText = if (this is EpisodeResponse) {
txt(
@ -927,15 +928,20 @@ class ResultViewModel2 : ViewModel() {
) {
val isSubscribed = _subscribeStatus.value ?: return
val response = currentResponse ?: return
if (response !is EpisodeResponse) return
val currentId = currentId ?: return
// This might be a bit confusing, but even if the loadresponse is not a EpisodeResponse
// _subscribeStatus might be true.
if (isSubscribed) {
removeSubscribedData(currentId)
statusChangedCallback?.invoke(false)
_subscribeStatus.postValue(false)
_subscribeStatus.postValue(if (response is EpisodeResponse) false else null)
MainActivity.reloadLibraryEvent(true)
} else {
if (response !is EpisodeResponse) {
return
}
checkAndWarnDuplicates(
context,
LibraryListType.SUBSCRIPTIONS,
@ -980,8 +986,8 @@ class ResultViewModel2 : ViewModel() {
)
_subscribeStatus.postValue(true)
statusChangedCallback?.invoke(true)
MainActivity.reloadLibraryEvent(true)
}
}
}
@ -1692,14 +1698,8 @@ class ResultViewModel2 : ViewModel() {
LoadType.ExternalApp,
txt(R.string.episode_action_copy_link)
) { (result, index) ->
val act = activity ?: return@acquireSingleLink
val serviceClipboard =
(act.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)
?: return@acquireSingleLink
val link = result.links[index]
val clip = ClipData.newPlainText(link.name, link.url)
serviceClipboard.setPrimaryClip(clip)
showToast(R.string.copy_link_toast, Toast.LENGTH_SHORT)
clipboardHelper(txt(link.name), link.url)
}
}
@ -2051,12 +2051,15 @@ class ResultViewModel2 : ViewModel() {
}
private fun postSubscription(loadResponse: LoadResponse) {
if (loadResponse.isEpisodeBased()) {
val id = loadResponse.getId()
val data = getSubscribedData(id)
if (loadResponse.isEpisodeBased()) {
updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val isSubscribed = data != null
_subscribeStatus.postValue(isSubscribed)
_subscribeStatus.postValue(data != null)
}
// lets say that we have subscribed, then we must be able to unsubscribe no matter what
else if (data != null) {
_subscribeStatus.postValue(true)
}
}
@ -2464,7 +2467,7 @@ class ResultViewModel2 : ViewModel() {
ResumeProgress(
progress = (viewPos.position / 1000).toInt(),
maxProgress = (viewPos.duration / 1000).toInt(),
txt(R.string.resume_time_left, (viewPos.duration - viewPos.position) / (60_000))
txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins"))
)
}
@ -2590,6 +2593,7 @@ class ResultViewModel2 : ViewModel() {
override var posterHeaders: Map<String, String>? = null,
override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null,
val id : Int?,
) : LoadResponse
fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe {
@ -2599,7 +2603,7 @@ class ResultViewModel2 : ViewModel() {
val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi
val repo = APIRepository(api)
val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others,
posterUrl = searchResponse.posterUrl).apply {
posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply {
if (searchResponse is SyncAPI.LibraryItem) {
this.plot = searchResponse.plot
this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating
@ -2611,12 +2615,14 @@ class ResultViewModel2 : ViewModel() {
this.tags = searchResponse.tags
}
}
val mainId = searchResponse.id ?: response.getId()
val mainId = response.getId()
postSuccessful(
loadResponse = response,
mainId = mainId,
apiRepository = repo, updateEpisodes = false, updateFillers = false)
apiRepository = repo,
updateEpisodes = false,
updateFillers = false)
}
fun load(

View file

@ -6,7 +6,8 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.databinding.ResultSelectionBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
typealias SelectData = Pair<UiText?, Any>
@ -72,8 +73,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
fun bind(
data: SelectData, isSelected: Boolean, callback: (Any) -> Unit
) {
val isTrueTv = isTrueTvSettings()
if (isTrueTv) {
if (isLayout(TV)) {
item.isFocusable = true
item.isFocusableInTouchMode = true
}

View file

@ -54,8 +54,9 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
@ -107,13 +108,16 @@ class SearchFragment : Fragment() {
)
bottomSheetDialog?.ownShow()
val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search
binding = try {
val layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_search_tv else R.layout.fragment_search
val root = inflater.inflate(layout, container, false)
// TODO TRYCATCH
binding = FragmentSearchBinding.bind(root)
FragmentSearchBinding.bind(root)
} catch (t : Throwable) {
FragmentSearchBinding.inflate(inflater)
}
return root
return binding?.root
}
private fun fixGrid() {
@ -369,7 +373,7 @@ class SearchFragment : Fragment() {
selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList()
if (isTrueTvSettings()) {
if (isLayout(TV)) {
binding?.searchFilter?.isFocusable = true
binding?.searchFilter?.isFocusableInTouchMode = true
}

View file

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast
@ -10,7 +9,8 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
@ -56,7 +56,7 @@ object SearchHelper {
}
}
SEARCH_ACTION_SHOW_METADATA -> {
if(!isTvSettings()) { // we only want this on phone as UI is not done yet on tv
if(isLayout(PHONE)) { // we only want this on phone as UI is not done yet on tv
(activity as? MainActivity?)?.apply {
loadPopup(callback.card)
} ?: kotlin.run {

View file

@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.isMovieType
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
@ -164,7 +165,7 @@ object SearchResultBuilder {
bg.isFocusable = false
bg.isFocusableInTouchMode = false
if(!isTrueTvSettings()) {
if(!isLayout(TV)) {
bg.setOnClickListener {
click(it)
}
@ -207,7 +208,7 @@ object SearchResultBuilder {
*/
if (isTrueTvSettings()) {
if (isLayout(TV)) {
// bg.isFocusable = true
// bg.isFocusableInTouchMode = true
// bg.touchscreenBlocksFocus = false

View file

@ -0,0 +1,56 @@
package com.lagradost.cloudstream3.ui.settings
import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
object Globals {
var beneneCount = 0
const val PHONE : Int = 0b001
const val TV : Int = 0b010
const val EMULATOR : Int = 0b100
private const val INVALID = -1
private var layoutId = INVALID
private fun Context.getLayoutInt(): Int {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getInt(this.getString(R.string.app_layout_key), -1)
}
private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV
val model = Build.MODEL.lowercase()
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
"AFT"
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
}
private fun Context.layoutIntCorrected(): Int {
return when(getLayoutInt()) {
-1 -> if (isAutoTv()) TV else PHONE
0 -> PHONE
1 -> TV
2 -> EMULATOR
else -> PHONE
}
}
fun Context.updateTv() {
layoutId = layoutIntCorrected()
}
/** Returns true if the layout is any of the flags,
* so isLayout(TV or EMULATOR) is a valid statement for checking if the layout is in the emulator
* or tv. Auto will become the "TV" or the "PHONE" layout.
*
* Valid flags are: PHONE, TV, EMULATOR
* */
fun isLayout(flags: Int) : Boolean {
return (layoutId and flags) != 0
}
}

View file

@ -11,8 +11,10 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountManagmentBinding
@ -27,12 +29,17 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -71,7 +78,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
showAccountSwitch(activity, api)
}
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
binding.accountSwitchAccount.requestFocus()
}
}
@ -135,7 +142,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
binding.loginUsernameInput to api.requiresUsername
)
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
visibilityMap.forEach { (input, isVisible) ->
input.isVisible = isVisible
@ -256,6 +263,24 @@ class SettingsAccount : PreferenceFragmentCompat() {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_account, rootKey)
getPref(R.string.biometric_key)?.setOnPreferenceClickListener {
val authEnabled = PreferenceManager.getDefaultSharedPreferences(
context ?: return@setOnPreferenceClickListener false
)
.getBoolean(getString(R.string.biometric_key), false)
if (authEnabled) {
BackupUtils.backup(activity)
val title = activity?.getString(R.string.biometric_setting)
val warning = activity?.getString(R.string.biometric_warning)
activity?.showBottomDialogText(
title as String,
warning.html()
) { onDialogDismissedEvent }
}
true
}
val syncApis =
listOf(
R.string.mal_key to malApi,

View file

@ -1,9 +1,5 @@
package com.lagradost.cloudstream3.ui.settings
import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -16,7 +12,6 @@ import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.lagradost.cloudstream3.R
@ -24,7 +19,12 @@ import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
@ -32,10 +32,6 @@ import java.io.File
class SettingsFragment : Fragment() {
companion object {
var beneneCount = 0
private var isTv: Boolean = false
private var isTrueTv: Boolean = false
fun PreferenceFragmentCompat?.getPref(id: Int): Preference? {
if (this == null) return null
@ -52,12 +48,12 @@ class SettingsFragment : Fragment() {
* On TV you cannot properly scroll to the bottom of settings, this fixes that.
* */
fun PreferenceFragmentCompat.setPaddingBottom() {
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
listView?.setPadding(0, 0, 0, 100.toPx)
}
}
fun PreferenceFragmentCompat.setToolBarScrollFlags() {
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
val settingsAppbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
@ -66,7 +62,7 @@ class SettingsFragment : Fragment() {
}
}
fun Fragment?.setToolBarScrollFlags() {
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
val settingsAppbar = this?.view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
@ -85,7 +81,7 @@ class SettingsFragment : Fragment() {
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
fixPaddingStatusbar(settingsToolbar)
UIHelper.fixPaddingStatusbar(settingsToolbar)
}
fun Fragment?.setUpToolbar(@StringRes title: Int) {
@ -100,7 +96,7 @@ class SettingsFragment : Fragment() {
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
fixPaddingStatusbar(settingsToolbar)
UIHelper.fixPaddingStatusbar(settingsToolbar)
}
fun getFolderSize(dir: File): Long {
@ -116,55 +112,7 @@ class SettingsFragment : Fragment() {
return size
}
private fun Context.getLayoutInt(): Int {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getInt(this.getString(R.string.app_layout_key), -1)
}
private fun Context.isTvSettings(): Boolean {
var value = getLayoutInt()
if (value == -1) {
value = if (isAutoTv()) 1 else 0
}
return value == 1 || value == 2
}
private fun Context.isTrueTvSettings(): Boolean {
var value = getLayoutInt()
if (value == -1) {
value = if (isAutoTv()) 1 else 0
}
return value == 1
}
fun Context.updateTv() {
isTrueTv = isTrueTvSettings()
isTv = isTvSettings()
}
fun isTrueTvSettings(): Boolean {
return isTrueTv
}
fun isTvSettings(): Boolean {
return isTv
}
fun Context.isEmulatorSettings(): Boolean {
return getLayoutInt() == 2
}
private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV
val model = Build.MODEL.lowercase()
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
"AFT"
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
@ -189,8 +137,6 @@ class SettingsFragment : Fragment() {
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
val isTrueTv = isTrueTvSettings()
for (syncApi in accountManagers) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue
@ -218,7 +164,7 @@ class SettingsFragment : Fragment() {
setOnClickListener {
navigate(navigationId)
}
if (isTrueTv) {
if (isLayout(TV)) {
isFocusable = true
isFocusableInTouchMode = true
}
@ -226,9 +172,17 @@ class SettingsFragment : Fragment() {
}
// Default focus on TV
if (isTrueTv) {
if (isLayout(TV)) {
settingsGeneral.requestFocus()
}
}
val appVersion = getString(R.string.app_version)
val commitInfo = getString(R.string.commit_hash)
binding?.appVersionInfo?.setOnLongClickListener{
clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo")
true
}
}
}

View file

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.EasterEggMonke
import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
@ -378,30 +379,30 @@ class SettingsGeneral : PreferenceFragmentCompat() {
}
try {
SettingsFragment.beneneCount =
beneneCount =
settingsManager.getInt(getString(R.string.benene_count), 0)
getPref(R.string.benene_count)?.let { pref ->
pref.summary =
if (SettingsFragment.beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
R.string.benene_count_text
).format(
SettingsFragment.beneneCount
beneneCount
)
pref.setOnPreferenceClickListener {
try {
SettingsFragment.beneneCount++
if (SettingsFragment.beneneCount%20 == 0) {
beneneCount++
if (beneneCount%20 == 0) {
val intent = Intent(context, EasterEggMonke::class.java)
startActivity(intent)
}
settingsManager.edit().putInt(
getString(R.string.benene_count),
SettingsFragment.beneneCount
beneneCount
)
.apply()
it.summary =
getString(R.string.benene_count_text).format(SettingsFragment.beneneCount)
getString(R.string.benene_count_text).format(beneneCount)
} catch (e: Exception) {
logError(e)
}

View file

@ -9,11 +9,11 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog

View file

@ -1,10 +1,6 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.os.TransactionTooLargeException
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
@ -20,6 +16,7 @@ import com.lagradost.cloudstream3.databinding.LogcatBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.services.BackupWorkManager
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
@ -30,6 +27,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -117,22 +115,15 @@ class SettingsUpdates : PreferenceFragmentCompat() {
binding.text1.text = text
binding.copyBtt.setOnClickListener {
// Can crash on too much text
try {
val serviceClipboard =
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?)
?: return@setOnClickListener
val clip = ClipData.newPlainText("logcat", text)
serviceClipboard.setPrimaryClip(clip)
clipboardHelper(txt("Logcat"), text)
dialog.dismissSafe(activity)
} catch (e: TransactionTooLargeException) {
showToast(R.string.clipboard_too_large)
}
}
binding.clearBtt.setOnClickListener {
Runtime.getRuntime().exec("logcat -c")
dialog.dismissSafe(activity)
}
binding.saveBtt.setOnClickListener {
var fileStream: OutputStream? = null
try {
@ -153,9 +144,11 @@ class SettingsUpdates : PreferenceFragmentCompat() {
fileStream?.closeQuietly()
}
}
binding.closeBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
return@setOnPreferenceClickListener true
}

View file

@ -29,7 +29,8 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog
@ -97,7 +98,7 @@ class ExtensionsFragment : Fragment() {
nextLeft = R.id.nav_rail_view
)
if (!isTrueTvSettings())
if (!isLayout(TV))
binding?.addRepoButton?.let { button ->
button.post {
setPadding(
@ -286,7 +287,7 @@ class ExtensionsFragment : Fragment() {
}
}
val isTv = isTrueTvSettings()
val isTv = isLayout(TV)
binding?.apply {
addRepoButton.isGone = isTv
addRepoButtonImageviewHolder.isVisible = isTv

View file

@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
@ -44,7 +45,7 @@ class PluginAdapter(
private val plugins: MutableList<PluginViewData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item
val layout = if(isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item
val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return PluginViewHolder(

View file

@ -17,7 +17,9 @@ import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.settings.appLanguages
@ -155,7 +157,7 @@ class PluginsFragment : Fragment() {
pluginViewModel.handlePluginAction(activity, url, it, isLocal)
}
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
// Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that.
binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx)
}

View file

@ -1,22 +1,18 @@
package com.lagradost.cloudstream3.ui.settings.extensions
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
class RepoAdapter(
val isSetup: Boolean,
@ -28,7 +24,7 @@ class RepoAdapter(
private val repositories: MutableList<RepositoryData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate(
val layout = if (isLayout(TV)) RepositoryItemTvBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
@ -121,13 +117,9 @@ class RepoAdapter(
}
repositoryItemRoot.setOnLongClickListener {
val clipboardManager =
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?
clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT)
}
return@setOnLongClickListener true
val shareableRepoData = "${repositoryData.name} : \n ${repositoryData.url}"
clipboardHelper(txt(R.string.repo_copy_label), shareableRepoData)
true
}
mainText.text = repositoryData.name

View file

@ -11,7 +11,8 @@ import com.lagradost.cloudstream3.databinding.FragmentTestingBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
@ -62,7 +63,7 @@ class TestFragment : Fragment() {
}
}
if (isTrueTvSettings()) {
if (isLayout(TV)) {
providerTest.playPauseButton?.isFocusableInTouchMode = true
providerTest.playPauseButton?.requestFocus()
}
@ -75,7 +76,7 @@ class TestFragment : Fragment() {
fun focusRecyclerView() {
// Hack to make it possible to focus the recyclerview.
if (isTrueTvSettings()) {
if (isLayout(TV)) {
providerTestRecyclerView.requestFocus()
providerTestAppbar.setExpanded(false, true)
}

View file

@ -13,8 +13,8 @@ import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.fasterxml.jackson.annotation.JsonProperty
import androidx.media3.common.text.Cue
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.android.gms.cast.TextTrackStyle
import com.google.android.gms.cast.TextTrackStyle.*
import com.jaredrummler.android.colorpicker.ColorPickerDialog
@ -24,7 +24,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -173,7 +175,7 @@ class ChromecastSubtitlesFragment : Fragment() {
state = getCurrentSavedStyle()
context?.updateState()
val isTvSettings = isTvSettings()
val isTvSettings = isLayout(TV or EMULATOR)
fun View.setFocusableInTv() {
this.isFocusableInTouchMode = isTvSettings

View file

@ -28,7 +28,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -252,7 +254,7 @@ class SubtitlesFragment : Fragment() {
state = getCurrentSavedStyle()
context?.updateState()
val isTvTrueSettings = isTrueTvSettings()
val isTvTrueSettings = isLayout(TV)
fun View.setFocusableInTv() {
this.isFocusableInTouchMode = isTvTrueSettings

View file

@ -61,8 +61,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.ui.WebviewFragment
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -78,7 +77,6 @@ import okhttp3.Cache
import java.io.*
import java.net.URL
import java.net.URLDecoder
import kotlin.system.measureTimeMillis
object AppUtils {
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
@ -583,7 +581,7 @@ object AppUtils {
//private val viewModel: ResultViewModel by activityViewModels()
private fun getResultsId(): Int {
return if (isTvSettings()) {
return if (Globals.isLayout(Globals.TV or Globals.EMULATOR)) {
R.id.global_to_navigation_results_tv
} else {
R.id.global_to_navigation_results_phone
@ -707,7 +705,7 @@ object AppUtils {
* Sets the focus to the negative button when in TV and Emulator layout.
**/
fun AlertDialog.setDefaultFocus(buttonFocus: Int = DialogInterface.BUTTON_NEGATIVE) {
if (!isTvSettings()) return
if (!Globals.isLayout(Globals.TV or Globals.EMULATOR)) return
this.getButton(buttonFocus).run {
isFocusableInTouchMode = true
requestFocus()

View file

@ -32,7 +32,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.mapper
import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.VideoDownloadManager.setupStream
@ -65,12 +64,16 @@ object BackupUtils {
PLUGINS_KEY_LOCAL,
OPEN_SUBTITLES_USER_KEY,
"nginx_user", // Nginx user key
DOWNLOAD_EPISODE_CACHE,
"biometric_key", // can lock down users if backup is shared on a incompatible device
"nginx_user" // Nginx user key
)
/** false if blacklisted key */
/** false if key should not be contained in backup */
private fun String.isTransferable(): Boolean {
return !nonTransferableKeys.contains(this)
return !nonTransferableKeys.any { this.contains(it) }
}
private var restoreFileSelector: ActivityResultLauncher<Array<String>>? = null
@ -252,8 +255,12 @@ object BackupUtils {
map: Map<String, T>?,
isEditingAppSettings: Boolean = false
) {
map?.filter { it.key.isTransferable() }?.forEach {
setKeyRaw(it.key, it.value, isEditingAppSettings)
val editor = DataStore.editor(this, isEditingAppSettings)
map?.forEach {
if (it.key.isTransferable()) {
editor.setKeyRaw(it.key, it.value)
}
}
editor.apply()
}
}

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

@ -50,6 +50,28 @@ class PreferenceDelegate<T : Any>(
}
}
/** When inserting many keys use this function, this is because apply for every key is very expensive on memory */
data class Editor(
val editor : SharedPreferences.Editor
) {
/** Always remember to call apply after */
fun<T> setKeyRaw(path: String, value: T) {
when (value) {
is Boolean -> editor.putBoolean(path, value)
is Int -> editor.putInt(path, value)
is String -> editor.putString(path, value)
is Float -> editor.putFloat(path, value)
is Long -> editor.putLong(path, value)
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
}
}
fun apply() {
editor.apply()
System.gc()
}
}
object DataStore {
val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
@ -66,22 +88,10 @@ object DataStore {
return "${folder}/${path}"
}
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) {
try {
fun editor(context : Context, isEditingAppSettings: Boolean = false) : Editor {
val editor: SharedPreferences.Editor =
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
when (value) {
is Boolean -> editor.putBoolean(path, value)
is Int -> editor.putInt(path, value)
is String -> editor.putString(path, value)
is Float -> editor.putFloat(path, value)
is Long -> editor.putLong(path, value)
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
}
editor.apply()
} catch (e: Exception) {
logError(e)
}
if (isEditingAppSettings) context.getDefaultSharedPrefs().edit() else context.getSharedPrefs().edit()
return Editor(editor)
}
fun Context.getDefaultSharedPrefs(): SharedPreferences {

View file

@ -21,7 +21,9 @@ import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding
import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -54,7 +56,7 @@ object SingleSelectionHelper {
) {
if (this == null) return
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
val binding = OptionsPopupTvBinding.inflate(layoutInflater)
val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(binding.root)

View file

@ -5,6 +5,8 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.app.AppOpsManager
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
@ -14,12 +16,15 @@ import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.TransactionTooLargeException
import android.util.Log
import android.view.*
import android.view.ViewGroup.MarginLayoutParams
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.ListAdapter
import android.widget.ListView
import android.widget.Toast.LENGTH_LONG
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
@ -30,14 +35,12 @@ import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
@ -58,17 +61,20 @@ import com.bumptech.glide.request.target.Target
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.UiImage
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlin.math.roundToInt
object UIHelper {
val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()
val Float.toPx: Float get() = (this * Resources.getSystem().displayMetrics.density)
@ -123,6 +129,35 @@ object UIHelper {
)
}
fun clipboardHelper(label: UiText, text: CharSequence) {
val ctx = context ?: return
try {
ctx.let {
val clip = ClipData.newPlainText(label.asString(ctx), text)
val labelSuffix = txt(R.string.toast_copied).asString(ctx)
ctx.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
showToast("${label.asString(ctx)} $labelSuffix")
}
}
} catch (t: Throwable) {
Log.e("ClipboardService", "$t")
when (t) {
is SecurityException -> {
showToast(R.string.clipboard_permission_error)
}
is TransactionTooLargeException -> {
showToast(R.string.clipboard_too_large)
}
else -> {
showToast(R.string.clipboard_unknown_error, LENGTH_LONG)
}
}
}
}
/**
* Sets ListView height dynamically based on the height of the items.
@ -434,7 +469,7 @@ object UIHelper {
}
fun Context.getStatusBarHeight(): Int {
if (isTvSettings()) {
if (isLayout(Globals.TV or EMULATOR)) {
return 0
}
@ -532,12 +567,11 @@ object UIHelper {
WindowInsetsControllerCompat(window, View(this)).show(WindowInsetsCompat.Type.systemBars())
} else {*/ /** WINDOW COMPAT IS BUGGY DUE TO FU*KED UP PLAYER AND TRAILERS **/
Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
(View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
//}
changeStatusBarState(isEmulatorSettings())
changeStatusBarState(isLayout(EMULATOR))
}
fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean {

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">
<shape android:shape="rectangle">
<solid android:color="?attr/white" />
<corners android:radius="3dp"/>
<corners android:radius="@dimen/rounded_image_radius"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white_attr_20" />
<corners android:radius="3dp"/>
<corners android:radius="@dimen/rounded_image_radius"/>
</shape>
</item>
</selector>

View file

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

View file

@ -41,22 +41,34 @@
android:layout_marginStart="10dp"
android:orientation="vertical">
<FrameLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/resultview_preview_title"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_weight="1"
android:textColor="?attr/textColor"
android:textSize="16sp"
android:layout_gravity="start|center_vertical"
android:textStyle="bold"
android:layout_marginEnd="25dp"
tools:text="The Perfect Run">
tools:text="The Perfect Run" />
</TextView>
<ImageView
android:id="@+id/resultview_preview_subscribe"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_margin="5dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/subscribe_tooltip"
android:elevation="10dp"
android:nextFocusDown="@id/resultview_preview_bookmark"
android:src="@drawable/baseline_notifications_none_24"
app:tint="?attr/textColor" />
<ImageView
android:id="@+id/resultview_preview_favorite"
@ -66,11 +78,13 @@
android:layout_margin="5dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/favorite"
android:elevation="10dp"
android:nextFocusDown="@id/resultview_preview_bookmark"
android:src="@drawable/ic_baseline_favorite_border_24"
app:tint="?attr/textColor" />
</FrameLayout>
</LinearLayout>
<com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent"
@ -134,38 +148,38 @@
</LinearLayout>
<LinearLayout
android:padding="7dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="7dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/resultview_preview_bookmark"
style="@style/BlackButton"
android:layout_width="50dp"
android:layout_weight="1"
android:nextFocusUp="@id/resultview_preview_favorite"
android:nextFocusRight="@id/resultview_preview_more_info"
tools:visibility="visible"
android:nextFocusUp="@id/resultview_preview_favorite"
app:icon="@drawable/ic_baseline_bookmark_24"
tools:text="Bookmark"
style="@style/BlackButton"
android:layout_width="50dp" />
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/resultview_preview_more_info"
style="@style/WhiteButton"
android:layout_width="50dp"
android:layout_weight="1"
android:nextFocusUp="@id/resultview_preview_favorite"
android:nextFocusLeft="@id/resultview_preview_bookmark"
tools:visibility="visible"
app:icon="@drawable/ic_baseline_open_in_new_24"
android:nextFocusUp="@id/resultview_preview_favorite"
android:text="@string/home_more_info"
style="@style/WhiteButton"
app:icon="@drawable/ic_baseline_open_in_new_24"
android:layout_width="50dp" />
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>

View file

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

View file

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

View file

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

View file

@ -78,6 +78,30 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
</LinearLayout>
</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
android:id="@+id/result_loading_error"
@ -124,31 +148,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textColor="?attr/textColor" />
</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
android:id="@+id/result_finish_loading"
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_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="175dp">
android:layout_marginTop="225dp">
<TextView
android:id="@+id/result_title"
@ -175,7 +174,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textSize="25sp"
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
@ -221,157 +220,293 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textStyle="normal"
tools:text="5d 3h 30m" />
</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"
android:visibility="gone"
tools:visibility="visible">
<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"
android:visibility="gone"
tools:visibility="visible">
<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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<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>
android:layout_marginTop="10dp"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/right_layout"
@ -382,7 +517,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/linearLayout2"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UselessParent">
<com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent"
@ -439,8 +575,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:foreground="@drawable/outline_drawable"
android:maxLines="7"
android:focusable="true"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button"
android:nextFocusUp="@id/result_play_parent"
android:nextFocusDown="@id/result_cast_items"
android:padding="5dp"
android:requiresFadingEdge="vertical"
android:textColor="?attr/textColor"
@ -450,59 +586,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
<com.google.android.material.chip.ChipGroup
android:id="@+id/result_tag"
style="@style/ChipParent"
android:layout_width="match_parent"
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>
@ -513,10 +598,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:descendantFocusability="afterDescendants"
android:fadingEdge="horizontal"
android:focusable="false"
android:focusableInTouchMode="false"
android:nextFocusUp="@id/result_episodes_show"
android:nextFocusDown="@id/result_recommendations_filter_selection"
android:nextFocusUp="@id/result_description"
android:nextFocusDown="@id/result_recommendations_list"
android:orientation="horizontal"
android:paddingTop="5dp"
android:requiresFadingEdge="horizontal"
@ -525,8 +608,23 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
tools:listitem="@layout/cast_item"
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
android:id="@+id/result_recommendations_holder"
android:descendantFocusability="afterDescendants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
@ -540,7 +638,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_marginEnd="10dp"
android:nextFocusUp="@id/result_cast_items"
android:nextFocusDown="@id/result_recommendations_list"
android:descendantFocusability="afterDescendants"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
@ -563,7 +661,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_height="wrap_content"
android:clipToPadding="false"
android:descendantFocusability="afterDescendants"
android:nextFocusUp="@id/result_recommendations_filter_selection"
android:nextFocusUp="@id/result_cast_items"
android:orientation="vertical"
app:spanCount="8"
tools:listitem="@layout/search_result_grid" />
@ -576,7 +674,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_height="match_parent"
android:layout_gravity="end"
android:visibility="gone"
tools:visibility="visible">
tools:visibility="invisible">
<!--
@ -765,152 +863,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_width="match_parent"
android:layout_height="wrap_content"
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
android:id="@+id/result_poster_holder"
android:layout_width="wrap_content"
@ -1067,70 +1020,47 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:visibility="gone"
app:icon="@drawable/ic_baseline_add_24" />
<!--<LinearLayout
android:id="@+id/result_resume_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical"
android:visibility="gone"
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: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:orientation="horizontal">
android:layout_gravity="start"
android:animateLayoutChanges="true"
android:orientation="vertical"
tools:visibility="visible">
<ImageView
android:id="@+id/result_resume_series_button"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
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"
<LinearLayout
android:id="@+id/series_holder"
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" />
android:orientation="vertical">
<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>
</LinearLayout>-->
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View file

@ -105,9 +105,10 @@
android:text="@string/extensions" />
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/app_version_info"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:layout_gravity="center"
android:orientation="horizontal">
<TextView

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -84,6 +84,7 @@
<color name="colorPrimaryOrange">#CE8500</color>
<color name="colorPrimaryDandelionYellow">#F5BB00</color>
<color name="colorPrimaryCoolBlue">#408cac</color>
<color name="colorPrimaryLavender">#6F55AF</color>
<color name="colorTestPass">#48E484</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="rotate_video_key" translatable="false">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 -->
<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>
@ -173,8 +174,8 @@
<string name="sort_close">Close</string>
<string name="sort_clear">Clear</string>
<string name="sort_save">Save</string>
<string name="copyTitle">Title copied!</string>
<string name="copyRepoUrl">Repo URL copied!</string>
<string name="repo_copy_label">Repository name and URL</string>
<string name="toast_copied">copied!</string>
<string name="subscribe_tooltip">New episode notification</string>
<string name="result_search_tooltip">Search in other extensions</string>
<string name="recommendations_tooltip">Show recommendations</string>
@ -247,7 +248,7 @@
<string name="backup_failed_error_format">Error backing up %s</string>
<string name="search">Search</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="settings_info">Info</string>
<string name="advanced_search">Advanced Search</string>
@ -306,6 +307,7 @@
<string name="go_forward_30">+30</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_remaining" formatted="true">%s\nremaining</string>
<string name="status_ongoing">Ongoing</string>
<string name="status_completed">Completed</string>
<string name="status">Status</string>
@ -645,6 +647,8 @@
<string name="history">History</string>
<string name="enable_skip_op_from_database_des">Show skip popups for opening/ending</string>
<string name="clipboard_too_large">Too much text. Unable to save to clipboard.</string>
<string name="clipboard_permission_error">Error accessing Clipboard, Please try again.</string>
<string name="clipboard_unknown_error">Error copying, Please copy logcat and contact app support.</string>
<string name="action_mark_as_watched">Mark as watched</string>
<string name="action_remove_from_watched">Remove from watched</string>
<string name="confirm_exit_dialog">Are you sure you want to exit\?</string>
@ -745,4 +749,17 @@
<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">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>

View file

@ -383,6 +383,16 @@
<item name="android:colorAccent">@color/colorPrimaryCoolBlue</item>
</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">
<item name="android:progressDrawable">@drawable/abc_ratingbar_indicator_material</item>
@ -804,6 +814,35 @@
<item name="android:insetTop">0dp</item>
</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">
<item name="android:tag">@string/tv_no_focus_tag</item>
<item name="android:stateListAnimator">@null</item>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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" />
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/mal_logo"
@ -22,17 +16,18 @@
<Preference
android:icon="@drawable/open_subtitles_icon"
android:key="@string/opensubtitles_key" />
<!-- <Preference-->
<!-- android:key="@string/nginx_key"-->
<!-- android:icon="@drawable/nginx" />-->
<!-- <Preference-->
<!-- android:title="@string/nginx_info_title"-->
<!-- android:icon="@drawable/nginx_question"-->
<!-- android:summary="@string/nginx_info_summary">-->
<!-- <intent-->
<!-- android:action="android.intent.action.VIEW"-->
<!-- android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" />-->
<!-- </Preference>-->
<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" />
<SwitchPreferenceCompat
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>

View file

@ -4,6 +4,7 @@ buildscript {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")