Added quality profiles (#414)
* Added quality profiles * Better quality selection * Added profile bg and fixed some sources (#483) Co-authored-by: Blatzar <> --------- Co-authored-by: Lag <> Co-authored-by: Osten <11805592+LagradOst@users.noreply.github.com>
|
@ -58,7 +58,7 @@ open class DoodLaExtractor : ExtractorApi() {
|
|||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
trueUrl,
|
||||
this.name,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
|
|
|
@ -58,7 +58,7 @@ open class GuardareStream : ExtractorApi() {
|
|||
jsonVideoData.data.forEach {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
it.file + ".${it.type}",
|
||||
this.name,
|
||||
this.name,
|
||||
it.file + ".${it.type}",
|
||||
mainUrl,
|
||||
|
|
|
@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
|
|||
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
||||
return jsonvideodata.data.map {
|
||||
ExtractorLink(
|
||||
it.file+".${it.type}",
|
||||
this.name,
|
||||
this.name,
|
||||
it.file+".${it.type}",
|
||||
mainUrl,
|
||||
|
|
|
@ -39,6 +39,7 @@ import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
|||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
|
@ -108,8 +109,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
// get() = episodes.isNotEmpty()
|
||||
|
||||
// options for player
|
||||
protected var currentPrefQuality =
|
||||
Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
|
||||
|
||||
/**
|
||||
* Default profile 1
|
||||
* Decides how links should be sorted based on a priority system.
|
||||
* This will be set in runtime based on settings.
|
||||
**/
|
||||
protected var currentQualityProfile = 1
|
||||
// protected var currentPrefQuality =
|
||||
// Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
|
||||
protected var fastForwardTime = 10000L
|
||||
protected var androidTVInterfaceOffSeekTime = 10000L;
|
||||
protected var androidTVInterfaceOnSeekTime = 30000L;
|
||||
|
@ -1221,10 +1229,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
.toLong() * 1000L
|
||||
|
||||
androidTVInterfaceOffSeekTime =
|
||||
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_off_seek_key), 10)
|
||||
settingsManager.getInt(
|
||||
ctx.getString(R.string.android_tv_interface_off_seek_key),
|
||||
10
|
||||
)
|
||||
.toLong() * 1000L
|
||||
androidTVInterfaceOnSeekTime =
|
||||
settingsManager.getInt(ctx.getString(R.string.android_tv_interface_on_seek_key), 10)
|
||||
settingsManager.getInt(
|
||||
ctx.getString(R.string.android_tv_interface_on_seek_key),
|
||||
10
|
||||
)
|
||||
.toLong() * 1000L
|
||||
|
||||
navigationBarHeight = ctx.getNavigationBarHeight()
|
||||
|
@ -1257,10 +1271,20 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
ctx.getString(R.string.double_tap_pause_enabled_key),
|
||||
false
|
||||
)
|
||||
currentPrefQuality = settingsManager.getInt(
|
||||
ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key),
|
||||
currentPrefQuality
|
||||
)
|
||||
|
||||
val profiles = QualityDataHelper.getProfiles()
|
||||
val type = if (ctx.isUsingMobileData())
|
||||
QualityDataHelper.QualityProfileType.Data
|
||||
else QualityDataHelper.QualityProfileType.WiFi
|
||||
|
||||
currentQualityProfile =
|
||||
profiles.firstOrNull { it.type == type }?.id ?: profiles.firstOrNull()?.id
|
||||
?: currentQualityProfile
|
||||
|
||||
// currentPrefQuality = settingsManager.getInt(
|
||||
// ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key),
|
||||
// currentPrefQuality
|
||||
// )
|
||||
// useSystemBrightness =
|
||||
// settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitl
|
|||
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriority
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriorityDialog
|
||||
import com.lagradost.cloudstream3.ui.result.*
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||
|
@ -57,6 +61,7 @@ import kotlinx.coroutines.Job
|
|||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.math.abs
|
||||
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
companion object {
|
||||
|
@ -188,17 +193,31 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
player.addTimeStamps(listOf()) // clear stamps
|
||||
}
|
||||
|
||||
private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
||||
return currentLinks.sortedBy {
|
||||
val (linkData, _) = it
|
||||
var quality = linkData?.quality ?: Qualities.Unknown.value
|
||||
private fun closestQuality(target: Int?): Qualities {
|
||||
if (target == null) return Qualities.Unknown
|
||||
return Qualities.values().minBy { abs(it.value - target) }
|
||||
}
|
||||
|
||||
// we set all qualities above current max as reverse
|
||||
if (useQualitySettings && quality > currentPrefQuality) {
|
||||
quality = currentPrefQuality - quality - 1
|
||||
}
|
||||
// negative because we want to sort highest quality first
|
||||
-(quality)
|
||||
private fun getLinkPriority(
|
||||
qualityProfile: Int,
|
||||
link: Pair<ExtractorLink?, ExtractorUri?>
|
||||
): Int {
|
||||
val (linkData, _) = link
|
||||
|
||||
val qualityPriority = QualityDataHelper.getQualityPriority(
|
||||
qualityProfile,
|
||||
closestQuality(linkData?.quality)
|
||||
)
|
||||
val sourcePriority =
|
||||
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name)
|
||||
|
||||
// negative because we want to sort highest quality first
|
||||
return qualityPriority + sourcePriority
|
||||
}
|
||||
|
||||
private fun sortLinks(qualityProfile: Int): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
||||
return currentLinks.sortedBy {
|
||||
-getLinkPriority(qualityProfile, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -584,33 +603,39 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
var sourceIndex = 0
|
||||
var startSource = 0
|
||||
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||
|
||||
val sortedUrls = sortLinks(useQualitySettings = false)
|
||||
if (sortedUrls.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone = true
|
||||
} else {
|
||||
startSource = sortedUrls.indexOf(currentSelectedLink)
|
||||
sourceIndex = startSource
|
||||
fun refreshLinks(qualityProfile: Int) {
|
||||
sortedUrls = sortLinks(qualityProfile)
|
||||
if (sortedUrls.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
|
||||
true
|
||||
} else {
|
||||
startSource = sortedUrls.indexOf(currentSelectedLink)
|
||||
sourceIndex = startSource
|
||||
|
||||
val sourcesArrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
val sourcesArrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
|
||||
val name = link?.name ?: uri?.name ?: "NULL"
|
||||
"$name ${Qualities.getStringByInt(link?.quality)}"
|
||||
})
|
||||
sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
|
||||
val name = link?.name ?: uri?.name ?: "NULL"
|
||||
"$name ${Qualities.getStringByInt(link?.quality)}"
|
||||
})
|
||||
|
||||
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
providerList.adapter = sourcesArrayAdapter
|
||||
providerList.setSelection(sourceIndex)
|
||||
providerList.setItemChecked(sourceIndex, true)
|
||||
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
providerList.adapter = sourcesArrayAdapter
|
||||
providerList.setSelection(sourceIndex)
|
||||
providerList.setItemChecked(sourceIndex, true)
|
||||
|
||||
providerList.setOnItemClickListener { _, _, which, _ ->
|
||||
sourceIndex = which
|
||||
providerList.setItemChecked(which, true)
|
||||
providerList.setOnItemClickListener { _, _, which, _ ->
|
||||
sourceIndex = which
|
||||
providerList.setItemChecked(which, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshLinks(currentQualityProfile)
|
||||
|
||||
sourceDialog.setOnDismissListener {
|
||||
if (shouldDismiss) dismiss()
|
||||
selectSourceDialog = null
|
||||
|
@ -650,6 +675,29 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
sourceDialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
fun setProfileName(profile: Int) {
|
||||
sourceDialog.source_settings_btt.setText(
|
||||
QualityDataHelper.getProfileName(
|
||||
profile
|
||||
)
|
||||
)
|
||||
}
|
||||
setProfileName(currentQualityProfile)
|
||||
|
||||
sourceDialog.profiles_click_settings.setOnClickListener {
|
||||
val activity = activity ?: return@setOnClickListener
|
||||
QualityProfileDialog(
|
||||
activity,
|
||||
R.style.AlertDialogCustomBlack,
|
||||
currentLinks.mapNotNull { it.first },
|
||||
currentQualityProfile
|
||||
) { profile ->
|
||||
currentQualityProfile = profile.id
|
||||
setProfileName(profile.id)
|
||||
refreshLinks(profile.id)
|
||||
}.show()
|
||||
}
|
||||
|
||||
sourceDialog.subtitles_encoding_format?.apply {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
|
@ -847,7 +895,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
private fun startPlayer() {
|
||||
if (isActive) return // we don't want double load when you skip loading
|
||||
|
||||
val links = sortLinks()
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
|
@ -868,12 +916,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun hasNextMirror(): Boolean {
|
||||
val links = sortLinks()
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
|
||||
}
|
||||
|
||||
override fun nextMirror() {
|
||||
val links = sortLinks()
|
||||
val links = sortLinks(currentQualityProfile)
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
|
@ -1314,6 +1362,15 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val turnVisible = it.isNotEmpty()
|
||||
val wasGone = overlay_loading_skip_button?.isGone == true
|
||||
overlay_loading_skip_button?.isVisible = turnVisible
|
||||
|
||||
normalSafeApiCall {
|
||||
currentLinks.lastOrNull()?.let { last ->
|
||||
if (getLinkPriority(currentQualityProfile, last) >= QualityDataHelper.AUTO_SKIP_PRIORITY) {
|
||||
startPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (turnVisible && wasGone) {
|
||||
overlay_loading_skip_button?.requestFocus()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import kotlinx.android.synthetic.main.player_prioritize_item.view.*
|
||||
|
||||
data class SourcePriority<T>(
|
||||
val data: T,
|
||||
val name: String,
|
||||
var priority: Int
|
||||
)
|
||||
|
||||
class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
|
||||
AppUtils.DiffAdapter<SourcePriority<T>>(items) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return PriorityViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is PriorityViewHolder -> holder.bind(items[position])
|
||||
}
|
||||
}
|
||||
|
||||
class PriorityViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
fun <T> bind(item: SourcePriority<T>) {
|
||||
val plusButton: ImageView = itemView.add_button
|
||||
val subtractButton: ImageView = itemView.subtract_button
|
||||
val priorityText: TextView = itemView.priority_text
|
||||
val priorityNumber: TextView = itemView.priority_number
|
||||
priorityText.text = item.name
|
||||
|
||||
fun updatePriority() {
|
||||
priorityNumber.text = item.priority.toString()
|
||||
}
|
||||
|
||||
updatePriority()
|
||||
plusButton.setOnClickListener {
|
||||
// If someone clicks til the integer limit then they deserve to crash.
|
||||
item.priority++
|
||||
updatePriority()
|
||||
}
|
||||
|
||||
subtractButton.setOnClickListener {
|
||||
item.priority--
|
||||
updatePriority()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.result.UiImage
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.card_view
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.outline
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_image_background
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_text
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_mobile_data
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_wifi
|
||||
|
||||
class ProfilesAdapter(
|
||||
override val items: MutableList<QualityDataHelper.QualityProfile>,
|
||||
val usedProfile: Int,
|
||||
val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit,
|
||||
) :
|
||||
AppUtils.DiffAdapter<QualityDataHelper.QualityProfile>(
|
||||
items,
|
||||
comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile ->
|
||||
first.id == second.id
|
||||
}) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return ProfilesViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.player_quality_profile_item, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ProfilesViewHolder -> holder.bind(items[position], position)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentItem: Pair<Int, QualityDataHelper.QualityProfile>? = null
|
||||
|
||||
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
|
||||
return currentItem?.second
|
||||
}
|
||||
|
||||
inner class ProfilesViewHolder(
|
||||
itemView: View,
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
private val art = listOf(
|
||||
R.drawable.profile_bg_teal,
|
||||
R.drawable.profile_bg_blue,
|
||||
R.drawable.profile_bg_dark_blue,
|
||||
R.drawable.profile_bg_purple,
|
||||
R.drawable.profile_bg_pink,
|
||||
R.drawable.profile_bg_red,
|
||||
R.drawable.profile_bg_orange,
|
||||
)
|
||||
|
||||
fun bind(item: QualityDataHelper.QualityProfile, index: Int) {
|
||||
val priorityText: TextView = itemView.profile_text
|
||||
val profileBg: ImageView = itemView.profile_image_background
|
||||
val wifiText: TextView = itemView.text_is_wifi
|
||||
val dataText: TextView = itemView.text_is_mobile_data
|
||||
val outline: View = itemView.outline
|
||||
val cardView: View = itemView.card_view
|
||||
|
||||
priorityText.text = item.name.asString(itemView.context)
|
||||
dataText.isVisible = item.type == QualityDataHelper.QualityProfileType.Data
|
||||
wifiText.isVisible = item.type == QualityDataHelper.QualityProfileType.WiFi
|
||||
|
||||
fun setCurrentItem() {
|
||||
val prevIndex = currentItem?.first
|
||||
// Prevent UI bug when re-selecting the item quickly
|
||||
if (prevIndex == index) {
|
||||
return
|
||||
}
|
||||
currentItem = index to item
|
||||
clickCallback.invoke(prevIndex, index)
|
||||
}
|
||||
|
||||
outline.isVisible = currentItem?.second?.id == item.id
|
||||
|
||||
profileBg.setImage(UiImage.Drawable(art[index % art.size]), null, false) { palette ->
|
||||
val color = palette.getDarkVibrantColor(
|
||||
ContextCompat.getColor(
|
||||
itemView.context,
|
||||
R.color.dubColorBg
|
||||
)
|
||||
)
|
||||
wifiText.backgroundTintList = ColorStateList.valueOf(color)
|
||||
dataText.backgroundTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
val textStyle =
|
||||
if (item.id == usedProfile) {
|
||||
Typeface.BOLD
|
||||
} else {
|
||||
Typeface.NORMAL
|
||||
}
|
||||
|
||||
priorityText.setTypeface(null, textStyle)
|
||||
|
||||
cardView.setOnClickListener {
|
||||
setCurrentItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
object QualityDataHelper {
|
||||
private const val VIDEO_SOURCE_PRIORITY = "video_source_priority"
|
||||
private const val VIDEO_PROFILE_NAME = "video_profile_name"
|
||||
private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority"
|
||||
private const val VIDEO_PROFILE_TYPE = "video_profile_type"
|
||||
private const val DEFAULT_SOURCE_PRIORITY = 1
|
||||
/**
|
||||
* Automatically skip loading links once this priority is reached
|
||||
**/
|
||||
const val AUTO_SKIP_PRIORITY = 10
|
||||
|
||||
/**
|
||||
* Must be higher than amount of QualityProfileTypes
|
||||
**/
|
||||
private const val PROFILE_COUNT = 7
|
||||
|
||||
/**
|
||||
* Unique guarantees that there will always be one of this type in the profile list.
|
||||
**/
|
||||
enum class QualityProfileType(@StringRes val stringRes: Int, val unique: Boolean) {
|
||||
None(R.string.none, false),
|
||||
WiFi(R.string.wifi, true),
|
||||
Data(R.string.mobile_data, true)
|
||||
}
|
||||
|
||||
data class QualityProfile(
|
||||
val name: UiText,
|
||||
val id: Int,
|
||||
val type: QualityProfileType
|
||||
)
|
||||
|
||||
fun getSourcePriority(profile: Int, name: String?): Int {
|
||||
if (name == null) return DEFAULT_SOURCE_PRIORITY
|
||||
return getKey(
|
||||
"$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile",
|
||||
name,
|
||||
DEFAULT_SOURCE_PRIORITY
|
||||
) ?: DEFAULT_SOURCE_PRIORITY
|
||||
}
|
||||
|
||||
fun setSourcePriority(profile: Int, name: String, priority: Int) {
|
||||
setKey("$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, priority)
|
||||
}
|
||||
|
||||
fun setProfileName(profile: Int, name: String?) {
|
||||
val path = "$currentAccount/$VIDEO_PROFILE_NAME/$profile"
|
||||
if (name == null) {
|
||||
removeKey(path)
|
||||
} else {
|
||||
setKey(path, name.trim())
|
||||
}
|
||||
}
|
||||
|
||||
fun getProfileName(profile: Int): UiText {
|
||||
return getKey<String>("$currentAccount/$VIDEO_PROFILE_NAME/$profile")?.let { txt(it) }
|
||||
?: txt(R.string.profile_number, profile)
|
||||
}
|
||||
|
||||
fun getQualityPriority(profile: Int, quality: Qualities): Int {
|
||||
return getKey(
|
||||
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
|
||||
quality.value.toString(),
|
||||
quality.defaultPriority
|
||||
) ?: quality.defaultPriority
|
||||
}
|
||||
|
||||
fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) {
|
||||
setKey(
|
||||
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
|
||||
quality.value.toString(),
|
||||
priority
|
||||
)
|
||||
}
|
||||
|
||||
fun getQualityProfileType(profile: Int): QualityProfileType {
|
||||
return getKey("$currentAccount/$VIDEO_PROFILE_TYPE/$profile") ?: QualityProfileType.None
|
||||
}
|
||||
|
||||
fun setQualityProfileType(profile: Int, type: QualityProfileType?) {
|
||||
val path = "$currentAccount/$VIDEO_PROFILE_TYPE/$profile"
|
||||
if (type == QualityProfileType.None) {
|
||||
removeKey(path)
|
||||
} else {
|
||||
setKey(path, type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all quality profiles, always includes one profile with WiFi and Data
|
||||
* Must under all circumstances at least return one profile
|
||||
**/
|
||||
fun getProfiles(): List<QualityProfile> {
|
||||
val availableTypes = QualityProfileType.values().toMutableList()
|
||||
val profiles = (1..PROFILE_COUNT).map { profileNumber ->
|
||||
// Get the real type
|
||||
val type = getQualityProfileType(profileNumber)
|
||||
|
||||
// This makes it impossible to get more than one of each type
|
||||
// Duplicates will be turned to None
|
||||
val uniqueType = if (type.unique && !availableTypes.remove(type)) {
|
||||
QualityProfileType.None
|
||||
} else {
|
||||
type
|
||||
}
|
||||
|
||||
QualityProfile(
|
||||
getProfileName(profileNumber),
|
||||
profileNumber,
|
||||
uniqueType
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
/**
|
||||
* If no profile of this type exists: insert it on the earliest profile with None type
|
||||
**/
|
||||
fun insertType(
|
||||
list: MutableList<QualityProfile>,
|
||||
type: QualityProfileType
|
||||
) {
|
||||
if (list.any { it.type == type }) return
|
||||
val index =
|
||||
list.indexOfFirst { it.type == QualityProfileType.None }
|
||||
list.getOrNull(index)?.copy(type = type)
|
||||
?.let { fixed ->
|
||||
list.set(index, fixed)
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileType.values().forEach {
|
||||
if (it.unique) insertType(profiles, it)
|
||||
}
|
||||
|
||||
debugAssert({
|
||||
!QualityProfileType.values().all { type ->
|
||||
!type.unique || profiles.any { it.type == type }
|
||||
}
|
||||
}, { "All unique quality types do not exist" })
|
||||
|
||||
debugAssert({
|
||||
profiles.isEmpty()
|
||||
}, { "No profiles!" })
|
||||
|
||||
return profiles
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_dialog.*
|
||||
|
||||
class QualityProfileDialog(
|
||||
val activity: FragmentActivity,
|
||||
@StyleRes val themeRes: Int,
|
||||
private val links: List<ExtractorLink>,
|
||||
private val usedProfile: Int,
|
||||
private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit
|
||||
) : Dialog(activity, themeRes) {
|
||||
override fun show() {
|
||||
setContentView(R.layout.player_quality_profile_dialog)
|
||||
val profilesRecyclerView: RecyclerView = profiles_recyclerview
|
||||
val useBtt: View = use_btt
|
||||
val editBtt: View = edit_btt
|
||||
val cancelBtt: View = cancel_btt
|
||||
val defaultBtt: View = set_default_btt
|
||||
val currentProfileText: TextView = currently_selected_profile_text
|
||||
val selectedItemActionsHolder: View = selected_item_holder
|
||||
|
||||
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
|
||||
return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile()
|
||||
}
|
||||
|
||||
fun refreshProfiles() {
|
||||
currentProfileText.text = getProfileName(usedProfile).asString(context)
|
||||
(profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles())
|
||||
}
|
||||
|
||||
profilesRecyclerView.adapter = ProfilesAdapter(
|
||||
mutableListOf(),
|
||||
usedProfile,
|
||||
) { oldIndex: Int?, newIndex: Int ->
|
||||
profilesRecyclerView.adapter?.notifyItemChanged(newIndex)
|
||||
selectedItemActionsHolder.alpha = 1f
|
||||
if (oldIndex != null) {
|
||||
profilesRecyclerView.adapter?.notifyItemChanged(oldIndex)
|
||||
}
|
||||
}
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
editBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let { profile ->
|
||||
SourcePriorityDialog(context, themeRes, links, profile) {
|
||||
refreshProfiles()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultBtt.setOnClickListener {
|
||||
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
|
||||
val choices = QualityDataHelper.QualityProfileType.values()
|
||||
.filter { it != QualityDataHelper.QualityProfileType.None }
|
||||
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
|
||||
|
||||
activity.showBottomDialog(
|
||||
choiceNames,
|
||||
choices.indexOf(currentProfile.type),
|
||||
txt(R.string.set_default).asString(context),
|
||||
false,
|
||||
{},
|
||||
{ index ->
|
||||
val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog
|
||||
// Remove previous picks
|
||||
if (pickedChoice.unique) {
|
||||
getProfiles().filter { it.type == pickedChoice }.forEach {
|
||||
QualityDataHelper.setQualityProfileType(it.id, null)
|
||||
}
|
||||
}
|
||||
|
||||
QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice)
|
||||
refreshProfiles()
|
||||
})
|
||||
}
|
||||
|
||||
cancelBtt.setOnClickListener {
|
||||
this.dismissSafe()
|
||||
}
|
||||
|
||||
useBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let {
|
||||
profileSelectionCallback.invoke(it)
|
||||
this.dismissSafe()
|
||||
}
|
||||
}
|
||||
|
||||
super.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.impl.constraints.controllers.ConstraintController
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import kotlinx.android.synthetic.main.player_select_source_priority.*
|
||||
|
||||
class SourcePriorityDialog(
|
||||
ctx: Context,
|
||||
@StyleRes themeRes: Int,
|
||||
val links: List<ExtractorLink>,
|
||||
private val profile: QualityDataHelper.QualityProfile,
|
||||
/**
|
||||
* Notify that the profile overview should be updated, for example if the name has been updated
|
||||
* Should not be called excessively.
|
||||
**/
|
||||
private val updatedCallback: () -> Unit
|
||||
) : Dialog(ctx, themeRes) {
|
||||
override fun show() {
|
||||
setContentView(R.layout.player_select_source_priority)
|
||||
val sourcesRecyclerView: RecyclerView = sort_sources
|
||||
val qualitiesRecyclerView: RecyclerView = sort_qualities
|
||||
val profileText: EditText = profile_text_editable
|
||||
val saveBtt: View = save_btt
|
||||
val exitBtt: View = close_btt
|
||||
val helpBtt: View = help_btt
|
||||
|
||||
profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context))
|
||||
profileText.hint = txt(R.string.profile_number, profile.id).asString(context)
|
||||
|
||||
sourcesRecyclerView.adapter = PriorityAdapter(
|
||||
links.map { link ->
|
||||
SourcePriority(
|
||||
null,
|
||||
link.source,
|
||||
QualityDataHelper.getSourcePriority(profile.id, link.source)
|
||||
)
|
||||
}.distinctBy { it.name }.sortedBy { -it.priority }.toMutableList()
|
||||
)
|
||||
|
||||
qualitiesRecyclerView.adapter = PriorityAdapter(
|
||||
Qualities.values().mapNotNull {
|
||||
SourcePriority(
|
||||
it,
|
||||
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null },
|
||||
QualityDataHelper.getQualityPriority(profile.id, it)
|
||||
)
|
||||
}.sortedBy { -it.priority }.toMutableList()
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // We know the types
|
||||
saveBtt.setOnClickListener {
|
||||
val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter<Qualities>
|
||||
val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter<Nothing>
|
||||
|
||||
val qualities = qualityAdapter?.items ?: emptyList()
|
||||
val sources = sourcesAdapter?.items ?: emptyList()
|
||||
|
||||
qualities.forEach {
|
||||
val data = it.data as? Qualities ?: return@forEach
|
||||
QualityDataHelper.setQualityPriority(profile.id, data, it.priority)
|
||||
}
|
||||
|
||||
sources.forEach {
|
||||
QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority)
|
||||
}
|
||||
|
||||
qualityAdapter?.updateList(qualities.sortedBy { -it.priority })
|
||||
sourcesAdapter?.updateList(sources.sortedBy { -it.priority })
|
||||
|
||||
val savedProfileName = profileText.text.toString()
|
||||
if (savedProfileName.isBlank()) {
|
||||
QualityDataHelper.setProfileName(profile.id, null)
|
||||
} else {
|
||||
QualityDataHelper.setProfileName(profile.id, savedProfileName)
|
||||
}
|
||||
updatedCallback.invoke()
|
||||
}
|
||||
|
||||
exitBtt.setOnClickListener {
|
||||
this.dismissSafe()
|
||||
}
|
||||
|
||||
helpBtt.setOnClickListener {
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setMessage(R.string.quality_profile_help)
|
||||
}.show()
|
||||
}
|
||||
|
||||
super.show()
|
||||
}
|
||||
}
|
|
@ -72,7 +72,7 @@ sealed class UiImage {
|
|||
|
||||
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
|
||||
when (value) {
|
||||
is UiImage.Image -> setImageImage(value,fadeIn)
|
||||
is UiImage.Image -> setImageImage(value, fadeIn)
|
||||
is UiImage.Drawable -> setImageDrawable(value)
|
||||
null -> {
|
||||
this?.isVisible = false
|
||||
|
@ -88,7 +88,7 @@ fun ImageView?.setImageImage(value: UiImage.Image, fadeIn: Boolean = true) {
|
|||
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
|
||||
if (this == null) return
|
||||
this.isVisible = true
|
||||
setImageResource(value.resId)
|
||||
this.setImage(UiImage.Drawable(value.resId))
|
||||
}
|
||||
|
||||
@JvmName("imgNull")
|
||||
|
|
|
@ -117,7 +117,7 @@ object DataStoreHelper {
|
|||
/**
|
||||
* A datastore wide account for future implementations of a multiple account system
|
||||
**/
|
||||
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
|
||||
var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
|
||||
|
||||
fun getAllWatchStateIds(): List<Int>? {
|
||||
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
||||
|
|
|
@ -114,16 +114,16 @@ data class ExtractorSubtitleLink(
|
|||
*/
|
||||
val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
|
||||
|
||||
enum class Qualities(var value: Int) {
|
||||
Unknown(400),
|
||||
P144(144), // 144p
|
||||
P240(240), // 240p
|
||||
P360(360), // 360p
|
||||
P480(480), // 480p
|
||||
P720(720), // 720p
|
||||
P1080(1080), // 1080p
|
||||
P1440(1440), // 1440p
|
||||
P2160(2160); // 4k or 2160p
|
||||
enum class Qualities(var value: Int, val defaultPriority: Int) {
|
||||
Unknown(400, 4),
|
||||
P144(144, 0), // 144p
|
||||
P240(240, 2), // 240p
|
||||
P360(360, 3), // 360p
|
||||
P480(480, 4), // 480p
|
||||
P720(720, 5), // 720p
|
||||
P1080(1080, 6), // 1080p
|
||||
P1440(1440, 7), // 1440p
|
||||
P2160(2160, 8); // 4k or 2160p
|
||||
|
||||
companion object {
|
||||
fun getStringByInt(qual: Int?): String {
|
||||
|
@ -135,6 +135,14 @@ enum class Qualities(var value: Int) {
|
|||
else -> "${qual}p"
|
||||
}
|
||||
}
|
||||
fun getStringByIntFull(quality: Int): String {
|
||||
return when (quality) {
|
||||
0 -> "Auto"
|
||||
Unknown.value -> "Unknown"
|
||||
P2160.value -> "4K"
|
||||
else -> "${quality}p"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -250,17 +250,6 @@ object SingleSelectionHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun showBottomDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
name: String,
|
||||
showApply: Boolean,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (Int) -> Unit,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/** Only for a low amount of items */
|
||||
fun Activity?.showBottomDialog(
|
||||
items: List<String>,
|
||||
|
|
|
@ -44,12 +44,13 @@ 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 com.lagradost.cloudstream3.utils.GlideOptions.bitmapTransform
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
@ -188,11 +189,30 @@ object UIHelper {
|
|||
fadeIn: Boolean = true,
|
||||
colorCallback: ((Palette) -> Unit)? = null
|
||||
): Boolean {
|
||||
if (this == null || url.isNullOrBlank()) return false
|
||||
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(GlideUrl(url) { headers ?: emptyMap() })
|
||||
.load(glideImage)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).let { req ->
|
||||
if (fadeIn)
|
||||
|
@ -211,7 +231,13 @@ object UIHelper {
|
|||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
resource?.toBitmapOrNull()
|
||||
?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) }
|
||||
?.let { bitmap ->
|
||||
createPaletteAsync(
|
||||
identifier,
|
||||
bitmap,
|
||||
colorCallback
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
5
app/src/main/res/drawable/baseline_help_outline_24.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/white" 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="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
|
||||
</vector>
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:tint="?attr/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13H5v-2h14v2z"/>
|
||||
|
|
BIN
app/src/main/res/drawable/profile_bg_blue.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
app/src/main/res/drawable/profile_bg_dark_blue.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
app/src/main/res/drawable/profile_bg_orange.jpg
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
app/src/main/res/drawable/profile_bg_pink.jpg
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
app/src/main/res/drawable/profile_bg_purple.jpg
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src/main/res/drawable/profile_bg_red.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
app/src/main/res/drawable/profile_bg_teal.jpg
Normal file
After Width: | Height: | Size: 126 KiB |
48
app/src/main/res/layout/player_prioritize_item.xml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="15dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/priority_text"
|
||||
style="@style/NoCheckLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@+id/subtract_button"
|
||||
tools:text="hello" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/subtract_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/priority_number"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/baseline_remove_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/priority_number"
|
||||
style="@style/NoCheckLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/add_button"
|
||||
android:gravity="center"
|
||||
android:minWidth="50dp"
|
||||
android:textColor="?attr/textColor"
|
||||
tools:text="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/add_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_baseline_add_24" />
|
||||
|
||||
</RelativeLayout>
|
105
app/src/main/res/layout/player_quality_profile_dialog.xml
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/profile_text_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/profiles"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currently_selected_profile_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="15sp"
|
||||
tools:text="Profile 1" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/profiles_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/profile_button_bar"
|
||||
android:layout_below="@+id/profile_text_bar"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/player_quality_profile_item" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/profile_button_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:gravity="end|bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/selected_item_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:alpha="0.5">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/edit_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/edit" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/set_default_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/set_default" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/use_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/use" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/sort_cancel" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</RelativeLayout>
|
66
app/src/main/res/layout/player_quality_profile_item.xml
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:focusable="false">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
android:layout_marginStart="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:backgroundTint="?attr/primaryGrayBackground"
|
||||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.4"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profile_image_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.4"
|
||||
android:contentDescription="@string/profile_background_des"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<View
|
||||
android:id="@+id/outline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/outline"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:textSize="16sp"
|
||||
tools:text="@string/mobile_data" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_wifi"
|
||||
style="@style/DubButton"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/wifi"
|
||||
android:textColor="@color/textColor" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_mobile_data"
|
||||
style="@style/DubButton"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/mobile_data"
|
||||
android:textColor="@color/textColor" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -20,19 +21,44 @@
|
|||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:id="@+id/profiles_click_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/pick_source"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
android:background="@drawable/outline_drawable_less"
|
||||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/pick_source"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/source_settings_btt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center"
|
||||
android:minWidth="140dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="15sp"
|
||||
app:drawableEndCompat="@drawable/ic_outline_settings_24"
|
||||
tools:text="@string/profile_number" />
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/sort_providers"
|
||||
|
|
179
app/src/main/res/layout/player_select_source_priority.xml
Normal file
|
@ -0,0 +1,179 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_sources_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/pick_source"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sort_sources"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_less"
|
||||
android:nextFocusLeft="@id/sort_subtitles"
|
||||
android:nextFocusRight="@id/apply_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:layout_height="100dp"
|
||||
tools:listitem="@layout/player_prioritize_item" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_subtitles_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- android:id="@+id/subs_settings" android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/subtitles_click_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:paddingBottom="10dp"
|
||||
android:text="@string/qualities"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/help_btt"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/baseline_help_outline_24"
|
||||
android:contentDescription="@string/help" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/home_change_provider_img_des"
|
||||
android:src="@drawable/ic_outline_settings_24"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/sort_qualities"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_less"
|
||||
android:nextFocusLeft="@id/sort_providers"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:layout_height="200dp"
|
||||
tools:listfooter="@layout/sort_bottom_footer_add_choice"
|
||||
tools:listitem="@layout/player_prioritize_item" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/apply_btt_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/profile_text_editable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:inputType="text"
|
||||
android:maxLength="32"
|
||||
android:layout_marginHorizontal="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="@string/profile_number"
|
||||
android:autofillHints="username" />
|
||||
<!-- <ImageView-->
|
||||
<!-- android:layout_width="50dp"-->
|
||||
<!-- android:layout_height="50dp"-->
|
||||
<!-- android:padding="10dp"-->
|
||||
<!-- android:layout_gravity="center"-->
|
||||
<!-- android:src="@drawable/outline_edit_24" />-->
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
</Space>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/save_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_save" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/close_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_close" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -658,4 +658,23 @@
|
|||
<string name="subscription_new">Subscribed to %s</string>
|
||||
<string name="subscription_deleted">Unsubscribed from %s</string>
|
||||
<string name="subscription_episode_released">Episode %d released!</string>
|
||||
<string name="profile_number">Profile %d</string>
|
||||
<string name="wifi">Wi-Fi</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="set_default">Set default</string>
|
||||
<string name="use">Use</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="profiles">Profiles</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="quality_profile_help">
|
||||
Here you can change how the sources are ordered. If a video has a higher priority it will appear higher in the source selection.
|
||||
The sum of the source priority and the quality priority is the video priority.
|
||||
\n\nSource A: 3
|
||||
\nQuality B: 7
|
||||
\nWill have a combined video priority of 10.
|
||||
|
||||
\n\nNOTE: If the sum is 10 or more the player will automatically skip loading when that link is loaded!
|
||||
</string>
|
||||
<string name="qualities">Qualities</string>
|
||||
<string name="profile_background_des">Profile background</string>
|
||||
</resources>
|
|
@ -11,14 +11,14 @@
|
|||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_category_defaults">
|
||||
<Preference
|
||||
android:icon="@drawable/ic_baseline_hd_24"
|
||||
android:key="@string/quality_pref_key"
|
||||
android:title="@string/watch_quality_pref" />
|
||||
<Preference
|
||||
android:icon="@drawable/ic_baseline_hd_24"
|
||||
android:key="@string/quality_pref_mobile_data_key"
|
||||
android:title="@string/watch_quality_pref_data" />
|
||||
<!-- <Preference-->
|
||||
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
|
||||
<!-- android:key="@string/quality_pref_key"-->
|
||||
<!-- android:title="@string/watch_quality_pref" />-->
|
||||
<!-- <Preference-->
|
||||
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
|
||||
<!-- android:key="@string/quality_pref_mobile_data_key"-->
|
||||
<!-- android:title="@string/watch_quality_pref_data" />-->
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/netflix_play"
|
||||
|
|