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 @@ - - + + + + + + + +