mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Added quality profiles
This commit is contained in:
parent
76545f55c3
commit
a6fd25d2e1
19 changed files with 1080 additions and 79 deletions
|
@ -39,6 +39,7 @@ import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
|
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.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
|
@ -108,8 +109,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
// get() = episodes.isNotEmpty()
|
// get() = episodes.isNotEmpty()
|
||||||
|
|
||||||
// options for player
|
// 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 fastForwardTime = 10000L
|
||||||
protected var androidTVInterfaceOffSeekTime = 10000L;
|
protected var androidTVInterfaceOffSeekTime = 10000L;
|
||||||
protected var androidTVInterfaceOnSeekTime = 30000L;
|
protected var androidTVInterfaceOnSeekTime = 30000L;
|
||||||
|
@ -1221,10 +1229,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
.toLong() * 1000L
|
.toLong() * 1000L
|
||||||
|
|
||||||
androidTVInterfaceOffSeekTime =
|
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
|
.toLong() * 1000L
|
||||||
androidTVInterfaceOnSeekTime =
|
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
|
.toLong() * 1000L
|
||||||
|
|
||||||
navigationBarHeight = ctx.getNavigationBarHeight()
|
navigationBarHeight = ctx.getNavigationBarHeight()
|
||||||
|
@ -1257,10 +1271,20 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
ctx.getString(R.string.double_tap_pause_enabled_key),
|
ctx.getString(R.string.double_tap_pause_enabled_key),
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
currentPrefQuality = settingsManager.getInt(
|
|
||||||
ctx.getString(if (ctx.isUsingMobileData()) R.string.quality_pref_mobile_data_key else R.string.quality_pref_key),
|
val profiles = QualityDataHelper.getProfiles()
|
||||||
currentPrefQuality
|
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 =
|
// useSystemBrightness =
|
||||||
// settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
|
// 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.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
||||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
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.result.*
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||||
|
@ -57,6 +61,7 @@ import kotlinx.coroutines.Job
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
class GeneratorPlayer : FullScreenPlayer() {
|
class GeneratorPlayer : FullScreenPlayer() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -188,17 +193,31 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
player.addTimeStamps(listOf()) // clear stamps
|
player.addTimeStamps(listOf()) // clear stamps
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
private fun closestQuality(target: Int?): Qualities {
|
||||||
return currentLinks.sortedBy {
|
if (target == null) return Qualities.Unknown
|
||||||
val (linkData, _) = it
|
return Qualities.values().minBy { abs(it.value - target) }
|
||||||
var quality = linkData?.quality ?: Qualities.Unknown.value
|
|
||||||
|
|
||||||
// we set all qualities above current max as reverse
|
|
||||||
if (useQualitySettings && quality > currentPrefQuality) {
|
|
||||||
quality = currentPrefQuality - quality - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// negative because we want to sort highest quality first
|
||||||
-(quality)
|
return qualityPriority + sourcePriority
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortLinks(qualityProfile: Int): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
||||||
|
return currentLinks.sortedBy {
|
||||||
|
-getLinkPriority(qualityProfile, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -584,10 +603,13 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
var sourceIndex = 0
|
var sourceIndex = 0
|
||||||
var startSource = 0
|
var startSource = 0
|
||||||
|
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||||
|
|
||||||
val sortedUrls = sortLinks(useQualitySettings = false)
|
fun refreshLinks(qualityProfile: Int) {
|
||||||
|
sortedUrls = sortLinks(qualityProfile)
|
||||||
if (sortedUrls.isEmpty()) {
|
if (sortedUrls.isEmpty()) {
|
||||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone = true
|
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
|
||||||
|
true
|
||||||
} else {
|
} else {
|
||||||
startSource = sortedUrls.indexOf(currentSelectedLink)
|
startSource = sortedUrls.indexOf(currentSelectedLink)
|
||||||
sourceIndex = startSource
|
sourceIndex = startSource
|
||||||
|
@ -610,6 +632,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
providerList.setItemChecked(which, true)
|
providerList.setItemChecked(which, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshLinks(currentQualityProfile)
|
||||||
|
|
||||||
sourceDialog.setOnDismissListener {
|
sourceDialog.setOnDismissListener {
|
||||||
if (shouldDismiss) dismiss()
|
if (shouldDismiss) dismiss()
|
||||||
|
@ -650,6 +675,29 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
sourceDialog.dismissSafe(activity)
|
sourceDialog.dismissSafe(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setProfileName(profile: Int) {
|
||||||
|
sourceDialog.source_settings_btt.setText(
|
||||||
|
QualityDataHelper.getProfileName(
|
||||||
|
profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setProfileName(currentQualityProfile)
|
||||||
|
|
||||||
|
sourceDialog.source_settings_btt.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 {
|
sourceDialog.subtitles_encoding_format?.apply {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||||
|
|
||||||
|
@ -847,7 +895,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
private fun startPlayer() {
|
private fun startPlayer() {
|
||||||
if (isActive) return // we don't want double load when you skip loading
|
if (isActive) return // we don't want double load when you skip loading
|
||||||
|
|
||||||
val links = sortLinks()
|
val links = sortLinks(currentQualityProfile)
|
||||||
if (links.isEmpty()) {
|
if (links.isEmpty()) {
|
||||||
noLinksFound()
|
noLinksFound()
|
||||||
return
|
return
|
||||||
|
@ -868,12 +916,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasNextMirror(): Boolean {
|
override fun hasNextMirror(): Boolean {
|
||||||
val links = sortLinks()
|
val links = sortLinks(currentQualityProfile)
|
||||||
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
|
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nextMirror() {
|
override fun nextMirror() {
|
||||||
val links = sortLinks()
|
val links = sortLinks(currentQualityProfile)
|
||||||
if (links.isEmpty()) {
|
if (links.isEmpty()) {
|
||||||
noLinksFound()
|
noLinksFound()
|
||||||
return
|
return
|
||||||
|
@ -1314,6 +1362,15 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
val turnVisible = it.isNotEmpty()
|
val turnVisible = it.isNotEmpty()
|
||||||
val wasGone = overlay_loading_skip_button?.isGone == true
|
val wasGone = overlay_loading_skip_button?.isGone == true
|
||||||
overlay_loading_skip_button?.isVisible = turnVisible
|
overlay_loading_skip_button?.isVisible = turnVisible
|
||||||
|
|
||||||
|
normalSafeApiCall {
|
||||||
|
currentLinks.lastOrNull()?.let { last ->
|
||||||
|
if (getLinkPriority(currentQualityProfile, last) >= QualityDataHelper.AUTO_SKIP_PRIORITY) {
|
||||||
|
startPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (turnVisible && wasGone) {
|
if (turnVisible && wasGone) {
|
||||||
overlay_loading_skip_button?.requestFocus()
|
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,81 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import kotlinx.android.synthetic.main.player_quality_profile_item.view.*
|
||||||
|
|
||||||
|
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) {
|
||||||
|
fun bind(item: QualityDataHelper.QualityProfile, index: Int) {
|
||||||
|
val priorityText: TextView = itemView.profile_text
|
||||||
|
val wifiText: TextView = itemView.text_is_wifi
|
||||||
|
val dataText: TextView = itemView.text_is_mobile_data
|
||||||
|
val outline: View = itemView.outline
|
||||||
|
|
||||||
|
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
|
||||||
|
currentItem = index to item
|
||||||
|
clickCallback.invoke(prevIndex, index)
|
||||||
|
}
|
||||||
|
outline.isVisible = currentItem?.second?.id == item.id
|
||||||
|
|
||||||
|
|
||||||
|
val textStyle =
|
||||||
|
if (item.id == usedProfile) {
|
||||||
|
Typeface.BOLD
|
||||||
|
} else {
|
||||||
|
Typeface.NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
priorityText.setTypeface(null, textStyle)
|
||||||
|
|
||||||
|
itemView.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 = 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,7 +117,7 @@ object DataStoreHelper {
|
||||||
/**
|
/**
|
||||||
* A datastore wide account for future implementations of a multiple account system
|
* 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>? {
|
fun getAllWatchStateIds(): List<Int>? {
|
||||||
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
val folder = "$currentAccount/$RESULT_WATCH_STATE"
|
||||||
|
|
|
@ -97,16 +97,16 @@ data class ExtractorSubtitleLink(
|
||||||
*/
|
*/
|
||||||
val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
|
val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
|
||||||
|
|
||||||
enum class Qualities(var value: Int) {
|
enum class Qualities(var value: Int, val defaultPriority: Int) {
|
||||||
Unknown(400),
|
Unknown(400, 4),
|
||||||
P144(144), // 144p
|
P144(144, 0), // 144p
|
||||||
P240(240), // 240p
|
P240(240, 2), // 240p
|
||||||
P360(360), // 360p
|
P360(360, 3), // 360p
|
||||||
P480(480), // 480p
|
P480(480, 4), // 480p
|
||||||
P720(720), // 720p
|
P720(720, 5), // 720p
|
||||||
P1080(1080), // 1080p
|
P1080(1080, 6), // 1080p
|
||||||
P1440(1440), // 1440p
|
P1440(1440, 7), // 1440p
|
||||||
P2160(2160); // 4k or 2160p
|
P2160(2160, 8); // 4k or 2160p
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getStringByInt(qual: Int?): String {
|
fun getStringByInt(qual: Int?): String {
|
||||||
|
@ -118,6 +118,14 @@ enum class Qualities(var value: Int) {
|
||||||
else -> "${qual}p"
|
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 */
|
/** Only for a low amount of items */
|
||||||
fun Activity?.showBottomDialog(
|
fun Activity?.showBottomDialog(
|
||||||
items: List<String>,
|
items: List<String>,
|
||||||
|
|
5
app/src/main/res/drawable/baseline_help_outline_24.xml
Normal file
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:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24"
|
android:viewportHeight="24"
|
||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/white">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M19,13H5v-2h14v2z"/>
|
android:pathData="M19,13H5v-2h14v2z"/>
|
||||||
|
|
47
app/src/main/res/layout/player_prioritize_item.xml
Normal file
47
app/src/main/res/layout/player_prioritize_item.xml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<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: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"
|
||||||
|
android:paddingStart="25dp"
|
||||||
|
android:paddingEnd="25dp"
|
||||||
|
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: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:padding="10dp"
|
||||||
|
android:src="@drawable/ic_baseline_add_24" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
99
app/src/main/res/layout/player_quality_profile_dialog.xml
Normal file
99
app/src/main/res/layout/player_quality_profile_dialog.xml
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<?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:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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:orientation="horizontal"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/player_quality_profile_item" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
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:alpha="0.5"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
42
app/src/main/res/layout/player_quality_profile_item.xml
Normal file
42
app/src/main/res/layout/player_quality_profile_item.xml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView 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:id="@+id/card_view"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="150dp"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:backgroundTint="?attr/primaryGrayBackground"
|
||||||
|
app:cardCornerRadius="@dimen/rounded_image_radius">
|
||||||
|
|
||||||
|
<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:layout_gravity="center"
|
||||||
|
android:id="@+id/profile_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:padding="10dp"
|
||||||
|
tools:text="Mobile data" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_is_wifi"
|
||||||
|
style="@style/DubButton"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:text="@string/wifi" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_is_mobile_data"
|
||||||
|
style="@style/DubButton"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:text="@string/mobile_data" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -20,20 +21,45 @@
|
||||||
android:layout_weight="50"
|
android:layout_weight="50"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_rowWeight="1"
|
android:layout_rowWeight="1"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
android:background="@drawable/outline_drawable_less"
|
||||||
|
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
android:paddingBottom="10dp"
|
|
||||||
android:text="@string/pick_source"
|
android:text="@string/pick_source"
|
||||||
android:textColor="?attr/textColor"
|
android:textColor="?attr/textColor"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
tools:text="Profile 1"
|
||||||
|
android:minWidth="140dp"
|
||||||
|
android:id="@+id/source_settings_btt"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:drawablePadding="10dp"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:drawableEndCompat="@drawable/ic_outline_settings_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/sort_providers"
|
android:id="@+id/sort_providers"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
178
app/src/main/res/layout/player_select_source_priority.xml
Normal file
178
app/src/main/res/layout/player_select_source_priority.xml
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
<?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:background="@drawable/outline_drawable_less"
|
||||||
|
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/qualities"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/help_btt"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:src="@drawable/baseline_help_outline_24"
|
||||||
|
android:layout_height="50dp">
|
||||||
|
</ImageView>
|
||||||
|
|
||||||
|
</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:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="Profile 1" />
|
||||||
|
<!-- <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>
|
|
@ -657,4 +657,19 @@
|
||||||
<string name="subscription_new">Subscribed to %s</string>
|
<string name="subscription_new">Subscribed to %s</string>
|
||||||
<string name="subscription_deleted">Unsubscribed from %s</string>
|
<string name="subscription_deleted">Unsubscribed from %s</string>
|
||||||
<string name="subscription_episode_released">Episode %d released!</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\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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -11,14 +11,14 @@
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/pref_category_defaults">
|
android:title="@string/pref_category_defaults">
|
||||||
<Preference
|
<!-- <Preference-->
|
||||||
android:icon="@drawable/ic_baseline_hd_24"
|
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
|
||||||
android:key="@string/quality_pref_key"
|
<!-- android:key="@string/quality_pref_key"-->
|
||||||
android:title="@string/watch_quality_pref" />
|
<!-- android:title="@string/watch_quality_pref" />-->
|
||||||
<Preference
|
<!-- <Preference-->
|
||||||
android:icon="@drawable/ic_baseline_hd_24"
|
<!-- android:icon="@drawable/ic_baseline_hd_24"-->
|
||||||
android:key="@string/quality_pref_mobile_data_key"
|
<!-- android:key="@string/quality_pref_mobile_data_key"-->
|
||||||
android:title="@string/watch_quality_pref_data" />
|
<!-- android:title="@string/watch_quality_pref_data" />-->
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/netflix_play"
|
android:icon="@drawable/netflix_play"
|
||||||
|
|
Loading…
Reference in a new issue