package com.lagradost.cloudstream3.utils import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.AppOpsManager import android.app.Dialog import android.content.Context import android.content.pm.PackageManager import android.content.res.Configuration import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.ListAdapter import android.widget.ListView import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat 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.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target 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 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) val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) fun Activity.checkWrite(): Boolean { return (ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_GRANTED // Since Android 13, we can't request external storage permission, // so don't check it. || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) } fun Activity.requestRW() { ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.MANAGE_EXTERNAL_STORAGE ), 1337 ) } /** * Sets ListView height dynamically based on the height of the items. * * @param listView to be resized * @return true if the listView is successfully resized, false otherwise */ fun setListViewHeightBasedOnItems(listView: ListView?) { val listAdapter: ListAdapter = listView?.adapter ?: return val numberOfItems: Int = listAdapter.count // Get total height of all items. var totalItemsHeight = 0 for (itemPos in 0 until numberOfItems) { val item: View = listAdapter.getView(itemPos, null, listView) item.measure(0, 0) totalItemsHeight += item.measuredHeight } // Get total height of all item dividers. val totalDividersHeight: Int = listView.dividerHeight * (numberOfItems - 1) // Set list height. val params: ViewGroup.LayoutParams = listView.layoutParams params.height = totalItemsHeight + totalDividersHeight listView.layoutParams = params listView.requestLayout() } fun Context?.getSpanCount(): Int? { val compactView = false val spanCountLandscape = if (compactView) 2 else 6 val spanCountPortrait = if (compactView) 1 else 3 val orientation = this?.resources?.configuration?.orientation ?: return null return if (orientation == Configuration.ORIENTATION_LANDSCAPE) { spanCountLandscape } else { spanCountPortrait } } fun Fragment.hideKeyboard() { activity?.window?.decorView?.clearFocus() view?.let { hideKeyboard(it) } } fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { hideKeyboard(it) } } fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) { try { if (this is FragmentActivity) { (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.navController?.navigate( navigation, arguments ) } } catch (t: Throwable) { logError(t) } } @ColorInt fun Context.getResourceColor(@AttrRes resource: Int, alphaFactor: Float = 1f): Int { val typedArray = obtainStyledAttributes(intArrayOf(resource)) val color = typedArray.getColor(0, 0) typedArray.recycle() if (alphaFactor < 1f) { val alpha = (color.alpha * alphaFactor).roundToInt() return Color.argb(alpha, color.red, color.green, color.blue) } return color } var createPaletteAsyncCache: HashMap = hashMapOf() fun createPaletteAsync(url: String, bitmap: Bitmap, callback: (Palette) -> Unit) { createPaletteAsyncCache[url]?.let { palette -> callback.invoke(palette) return } Palette.from(bitmap).generate { paletteNull -> paletteNull?.let { palette -> createPaletteAsyncCache[url] = palette callback(palette) } } } fun ImageView?.setImage( url: String?, headers: Map? = null, @DrawableRes errorImageDrawable: Int? = null, fadeIn: Boolean = true, colorCallback: ((Palette) -> Unit)? = null ): Boolean { if (url.isNullOrBlank()) return false this.setImage(UiImage.Image(url, headers, errorImageDrawable), errorImageDrawable, fadeIn, colorCallback) return true } fun ImageView?.setImage( uiImage: UiImage?, @DrawableRes errorImageDrawable: Int? = null, fadeIn: Boolean = true, colorCallback: ((Palette) -> Unit)? = null ): Boolean { if (this == null || uiImage == null) return false val (glideImage, identifier) = (uiImage as? UiImage.Drawable)?.resId?.let { it to it.toString() } ?: (uiImage as? UiImage.Image)?.let { image -> GlideUrl(image.url) { image.headers ?: emptyMap() } to image.url } ?: return false return try { val builder = GlideApp.with(this) .load(glideImage) .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.ALL).let { req -> if (fadeIn) req.transition(DrawableTransitionOptions.withCrossFade()) else req } if (colorCallback != null) { builder.listener(object : RequestListener { @SuppressLint("CheckResult") override fun onResourceReady( resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean ): Boolean { resource?.toBitmapOrNull() ?.let { bitmap -> createPaletteAsync( identifier, bitmap, colorCallback ) } return false } @SuppressLint("CheckResult") override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean ): Boolean { return false } }) } val res = if (errorImageDrawable != null) builder.error(errorImageDrawable).into(this) else builder.into(this) res.clearOnDetach() true } catch (e: Exception) { logError(e) false } } fun ImageView?.setImageBlur( url: String?, radius: Int, sample: Int = 3, headers: Map? = null ) { if (this == null || url.isNullOrBlank()) return try { val res = GlideApp.with(this) .load(GlideUrl(url) { headers ?: emptyMap() }) .apply(bitmapTransform(BlurTransformation(radius, sample))) .transition( DrawableTransitionOptions.withCrossFade() ) .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(this) res.clearOnDetach() } catch (e: Exception) { logError(e) } } fun adjustAlpha(@ColorInt color: Int, factor: Float): Int { val alpha = (Color.alpha(color) * factor).roundToInt() val red = Color.red(color) val green = Color.green(color) val blue = Color.blue(color) return Color.argb(alpha, red, green, blue) } fun Context.colorFromAttribute(attribute: Int): Int { val attributes = obtainStyledAttributes(intArrayOf(attribute)) val color = attributes.getColor(0, 0) attributes.recycle() return color } fun Activity.hideSystemUI() { // Enables regular immersive mode. // For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE. // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY window.decorView.systemUiVisibility = ( View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY // Set the content to appear under the system bars so that the // content doesn't resize when the system bars hide and show. or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // Hide the nav bar and status bar or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN // or View.SYSTEM_UI_FLAG_LOW_PROFILE ) // window.addFlags(View.KEEP_SCREEN_ON) } fun FragmentActivity.popCurrentPage() { this.onBackPressed() /*val currentFragment = supportFragmentManager.fragments.lastOrNull { it.isVisible } ?: return supportFragmentManager.beginTransaction() .setCustomAnimations( R.anim.enter_anim, R.anim.exit_anim, R.anim.pop_enter, R.anim.pop_exit ) .remove(currentFragment) .commitAllowingStateLoss()*/ } /* fun FragmentActivity.popCurrentPage(isInPlayer: Boolean, isInExpandedView: Boolean, isInResults: Boolean) { val currentFragment = supportFragmentManager.fragments.lastOrNull { it.isVisible } ?: //this.onBackPressed() return /* if (tvActivity == null) { requestedOrientation = if (settingsManager?.getBoolean("force_landscape", false) == true) { ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE } else { ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } }*/ // No fucked animations leaving the player :) when { isInPlayer -> { supportFragmentManager.beginTransaction() //.setCustomAnimations(R.anim.enter, R.anim.exit, R.anim.pop_enter, R.anim.pop_exit) .remove(currentFragment) .commitAllowingStateLoss() } isInExpandedView && !isInResults -> { supportFragmentManager.beginTransaction() .setCustomAnimations( R.anim.enter_anim,//R.anim.enter_from_right, R.anim.exit_anim,//R.anim.exit_to_right, R.anim.pop_enter, R.anim.pop_exit ) .remove(currentFragment) .commitAllowingStateLoss() } else -> { supportFragmentManager.beginTransaction() .setCustomAnimations(R.anim.enter_anim, R.anim.exit_anim, R.anim.pop_enter, R.anim.pop_exit) .remove(currentFragment) .commitAllowingStateLoss() } } }*/ fun Context.getStatusBarHeight(): Int { if (isTvSettings()) { return 0 } var result = 0 val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") if (resourceId > 0) { result = resources.getDimensionPixelSize(resourceId) } return result } fun Context?.fixPaddingStatusbar(v: View?) { if (v == null || this == null) return v.setPadding( v.paddingLeft, v.paddingTop + getStatusBarHeight(), v.paddingRight, v.paddingBottom ) } fun Context.fixPaddingStatusbarView(v: View?) { if (v == null) return val params = v.layoutParams params.height = getStatusBarHeight() v.layoutParams = params } fun Context.getNavigationBarHeight(): Int { var result = 0 val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") if (resourceId > 0) { result = resources.getDimensionPixelSize(resourceId) } return result } fun Context?.IsBottomLayout(): Boolean { if (this == null) return true val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) return settingsManager.getBoolean(getString(R.string.bottom_title_key), true) } fun Activity.changeStatusBarState(hide: Boolean): Int { return if (hide) { window?.setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN ) 0 } else { window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) this.getStatusBarHeight() } } // Shows the system bars by removing all the flags // except for the ones that make the content appear under the system bars. fun Activity.showSystemUI() { window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) changeStatusBarState(isEmulatorSettings()) // window.clearFlags(View.KEEP_SCREEN_ON) } fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean { return try { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) settingsManager?.getBoolean( getString(R.string.pip_enabled_key), true ) ?: true && isInPlayer } catch (e: Exception) { logError(e) false } } fun Context.hasPIPPermission(): Boolean { val appOps = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { appOps.checkOpNoThrow( AppOpsManager.OPSTR_PICTURE_IN_PICTURE, android.os.Process.myUid(), packageName ) == AppOpsManager.MODE_ALLOWED } else { return true } } fun hideKeyboard(view: View?) { if (view == null) return val inputMethodManager = view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager? inputMethodManager?.hideSoftInputFromWindow(view.windowToken, 0) } fun showInputMethod(view: View?) { if (view == null) return val inputMethodManager = view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager? inputMethodManager?.showSoftInput(view, 0) } fun Dialog?.dismissSafe(activity: Activity?) { if (this?.isShowing == true && activity?.isFinishing == false) { this.dismiss() } } fun Dialog?.dismissSafe() { if (this?.isShowing == true) { this.dismiss() } } /**id, stringRes */ @SuppressLint("RestrictedApi") fun View.popupMenuNoIcons( items: List>, onMenuItemClick: MenuItem.() -> Unit, ): PopupMenu { val ctw = ContextThemeWrapper(context, R.style.PopupMenu) val popup = PopupMenu(ctw, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0) items.forEach { (id, stringRes) -> popup.menu.add(0, id, 0, stringRes) } (popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true) popup.setOnMenuItemClickListener { it.onMenuItemClick() true } popup.show() return popup } /**id, string */ @SuppressLint("RestrictedApi") fun View.popupMenuNoIconsAndNoStringRes( items: List>, onMenuItemClick: MenuItem.() -> Unit, ): PopupMenu { val ctw = ContextThemeWrapper(context, R.style.PopupMenu) val popup = PopupMenu(ctw, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0) items.forEach { (id, string) -> popup.menu.add(0, id, 0, string) } (popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true) popup.setOnMenuItemClickListener { it.onMenuItemClick() true } popup.show() return popup } }