diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
index 0d94eb08..24495a40 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
@@ -58,7 +58,7 @@ open class DoodLaExtractor : ExtractorApi() {
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("
").substringBefore(""))?.groupValues?.get(0)
return listOf(
ExtractorLink(
- trueUrl,
+ this.name,
this.name,
trueUrl,
mainUrl,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
index 2adc00d5..3d046267 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
@@ -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,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
index d721dea8..13aa48c6 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
@@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
val jsonvideodata = parseJson(response)
return jsonvideodata.data.map {
ExtractorLink(
- it.file+".${it.type}",
+ this.name,
this.name,
it.file+".${it.type}",
mainUrl,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
index 86e21fd6..9ff1c52d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
@@ -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)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
index 46f2bca9..e20a07fa 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
@@ -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> {
- 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
+ ): 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> {
+ return currentLinks.sortedBy {
+ -getLinkPriority(qualityProfile, it)
}
}
@@ -584,33 +603,39 @@ class GeneratorPlayer : FullScreenPlayer() {
var sourceIndex = 0
var startSource = 0
+ var sortedUrls = emptyList>()
- val sortedUrls = sortLinks(useQualitySettings = false)
- if (sortedUrls.isEmpty()) {
- sourceDialog.findViewById(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(R.id.sort_sources_holder)?.isGone =
+ true
+ } else {
+ startSource = sortedUrls.indexOf(currentSelectedLink)
+ sourceIndex = startSource
- val sourcesArrayAdapter =
- ArrayAdapter(ctx, R.layout.sort_bottom_single_choice)
+ val sourcesArrayAdapter =
+ ArrayAdapter(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()
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt
new file mode 100644
index 00000000..8e0ce67c
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt
@@ -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(
+ val data: T,
+ val name: String,
+ var priority: Int
+)
+
+class PriorityAdapter(override val items: MutableList>) :
+ AppUtils.DiffAdapter>(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 bind(item: SourcePriority) {
+ 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()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt
new file mode 100644
index 00000000..ff84c1f5
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt
@@ -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,
+ val usedProfile: Int,
+ val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit,
+) :
+ AppUtils.DiffAdapter(
+ 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? = 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()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt
new file mode 100644
index 00000000..96249db4
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt
@@ -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("$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 {
+ 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,
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt
new file mode 100644
index 00000000..28a6365f
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt
@@ -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,
+ 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt
new file mode 100644
index 00000000..efc1f1b8
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt
@@ -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,
+ 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
+ val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter
+
+ 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
index 81ef8d57..f2eca5b8 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
@@ -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")
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt
index 516cd990..3bdb64e1 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt
@@ -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? {
val folder = "$currentAccount/$RESULT_WATCH_STATE"
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
index 5062ebd9..f6373dce 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
@@ -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"
+ }
+ }
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt
index 2dc6846c..1f6d726d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt
@@ -250,17 +250,6 @@ object SingleSelectionHelper {
)
}
- fun showBottomDialog(
- items: List,
- selectedIndex: Int,
- name: String,
- showApply: Boolean,
- dismissCallback: () -> Unit,
- callback: (Int) -> Unit,
- ) {
-
- }
-
/** Only for a low amount of items */
fun Activity?.showBottomDialog(
items: List,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
index c300d615..7d798204 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
@@ -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
}
diff --git a/app/src/main/res/drawable/baseline_help_outline_24.xml b/app/src/main/res/drawable/baseline_help_outline_24.xml
new file mode 100644
index 00000000..3a72cda0
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_help_outline_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_remove_24.xml b/app/src/main/res/drawable/baseline_remove_24.xml
index 791a2f81..f4455598 100644
--- a/app/src/main/res/drawable/baseline_remove_24.xml
+++ b/app/src/main/res/drawable/baseline_remove_24.xml
@@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
- android:tint="?attr/colorControlNormal">
+ android:tint="?attr/white">
diff --git a/app/src/main/res/drawable/profile_bg_blue.jpg b/app/src/main/res/drawable/profile_bg_blue.jpg
new file mode 100644
index 00000000..e573439b
Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_blue.jpg differ
diff --git a/app/src/main/res/drawable/profile_bg_dark_blue.jpg b/app/src/main/res/drawable/profile_bg_dark_blue.jpg
new file mode 100644
index 00000000..c6482bc7
Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_dark_blue.jpg differ
diff --git a/app/src/main/res/drawable/profile_bg_orange.jpg b/app/src/main/res/drawable/profile_bg_orange.jpg
new file mode 100644
index 00000000..ea638c8b
Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_orange.jpg differ
diff --git a/app/src/main/res/drawable/profile_bg_pink.jpg b/app/src/main/res/drawable/profile_bg_pink.jpg
new file mode 100644
index 00000000..63473fe0
Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_pink.jpg differ
diff --git a/app/src/main/res/drawable/profile_bg_purple.jpg b/app/src/main/res/drawable/profile_bg_purple.jpg
new file mode 100644
index 00000000..15723dba
Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_purple.jpg differ
diff --git a/app/src/main/res/drawable/profile_bg_red.jpg b/app/src/main/res/drawable/profile_bg_red.jpg
new file mode 100644
index 00000000..6a27ff31
Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_red.jpg differ
diff --git a/app/src/main/res/drawable/profile_bg_teal.jpg b/app/src/main/res/drawable/profile_bg_teal.jpg
new file mode 100644
index 00000000..75ef777b
Binary files /dev/null and b/app/src/main/res/drawable/profile_bg_teal.jpg differ
diff --git a/app/src/main/res/layout/player_prioritize_item.xml b/app/src/main/res/layout/player_prioritize_item.xml
new file mode 100644
index 00000000..b78863f8
--- /dev/null
+++ b/app/src/main/res/layout/player_prioritize_item.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/player_quality_profile_dialog.xml b/app/src/main/res/layout/player_quality_profile_dialog.xml
new file mode 100644
index 00000000..7bd7a680
--- /dev/null
+++ b/app/src/main/res/layout/player_quality_profile_dialog.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/player_quality_profile_item.xml b/app/src/main/res/layout/player_quality_profile_item.xml
new file mode 100644
index 00000000..3fad69ac
--- /dev/null
+++ b/app/src/main/res/layout/player_quality_profile_item.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml
index 067e4ad5..550b08d5 100644
--- a/app/src/main/res/layout/player_select_source_and_subs.xml
+++ b/app/src/main/res/layout/player_select_source_and_subs.xml
@@ -1,5 +1,6 @@
-
+ android:background="@drawable/outline_drawable_less"
+ android:foreground="?attr/selectableItemBackgroundBorderless"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8f67739d..fbaecd2e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -658,4 +658,23 @@
Subscribed to %s
Unsubscribed from %s
Episode %d released!
-
\ No newline at end of file
+ Profile %d
+ Wi-Fi
+ Mobile data
+ Set default
+ Use
+ Edit
+ Profiles
+ 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!
+
+ Qualities
+ Profile background
+
diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml
index 2d2905ea..ad33e036 100644
--- a/app/src/main/res/xml/settings_player.xml
+++ b/app/src/main/res/xml/settings_player.xml
@@ -11,14 +11,14 @@
-
-
+
+
+
+
+
+
+
+