From 7910e069874675271ed2050048037218206dae14 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:15:17 +0000 Subject: [PATCH 1/3] Fix subtitle selection --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 31 ++--- .../ui/player/PlayerSubtitleHelper.kt | 53 ++++++++ .../cloudstream3/SubtitleSelectionTest.kt | 120 ++++++++++++++++++ 3 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt 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 1bd0b158feb..7d188e55d69 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 @@ -181,17 +181,17 @@ class GeneratorPlayer : FullScreenPlayer() { binding?.playerLoadingOverlay?.isVisible = true } - private fun setSubtitles(subtitle: SubtitleData?): Boolean { - // If subtitle is changed -> Save the language - if (subtitle != currentSelectedSubtitles) { + private fun setSubtitles(subtitle: SubtitleData?, userInitiated: Boolean): Boolean { + // If subtitle is changed and user initiated -> Save the language + if (subtitle != currentSelectedSubtitles && userInitiated) { val subtitleLanguageTagIETF = if (subtitle == null) { "" // -> No Subtitles } else { - fromCodeToLangTagIETF(subtitle.languageCode) - ?: fromLanguageToTagIETF(subtitle.languageCode, halfMatch = true) + subtitle.getIETF_tag() } if (subtitleLanguageTagIETF != null) { + Log.i(TAG, "Set SUBTITLE_AUTO_SELECT_KEY to '$subtitleLanguageTagIETF'") setKey(SUBTITLE_AUTO_SELECT_KEY, subtitleLanguageTagIETF) preferredAutoSelectSubtitles = subtitleLanguageTagIETF } @@ -225,7 +225,7 @@ class GeneratorPlayer : FullScreenPlayer() { } private fun noSubtitles(): Boolean { - return setSubtitles(null) + return setSubtitles(null, true) } private fun getPos(): Long { @@ -909,7 +909,7 @@ class GeneratorPlayer : FullScreenPlayer() { player.saveData() player.reloadPlayer(ctx) - setSubtitles(selectedSubtitle) + setSubtitles(selectedSubtitle, false) viewModel.addSubtitles(subtitleData.toSet()) selectSourceDialog?.dismissSafe() @@ -1362,7 +1362,7 @@ class GeneratorPlayer : FullScreenPlayer() { subtitlesGroupedList.getOrNull(subtitleGroupIndex - 1)?.value?.getOrNull( subtitleOptionIndex )?.let { - setSubtitles(it) + setSubtitles(it, true) } ?: false } } @@ -1659,12 +1659,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } - private fun SubtitleData.matchesLanguage(langCode: String): Boolean { - val langName = fromTagToEnglishLanguageName(langCode) ?: return false - val cleanedName = originalName.replace(Regex("[^\\p{L}\\p{Mn}\\p{Mc}\\p{Me} ]"), "").trim() - return languageCode == langCode || cleanedName == langName || cleanedName.contains(langName) || cleanedName == langCode - } - private fun getAutoSelectSubtitle( subtitles: Set, settings: Boolean, downloads: Boolean ): SubtitleData? { @@ -1684,8 +1678,9 @@ class GeneratorPlayer : FullScreenPlayer() { val current = player.getCurrentPreferredSubtitle() Log.i(TAG, "autoSelectFromSettings = $current") context?.let { ctx -> - if (current != null) { - if (setSubtitles(current)) { + // Only use the player preferred subtitle if it matches the available language + if (current != null && (langCode == null || current.matchesLanguage(langCode))) { + if (setSubtitles(current, false)) { player.saveData() player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) @@ -1695,7 +1690,7 @@ class GeneratorPlayer : FullScreenPlayer() { getAutoSelectSubtitle( currentSubs, settings = true, downloads = false )?.let { sub -> - if (setSubtitles(sub)) { + if (setSubtitles(sub, false)) { player.saveData() player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) @@ -1711,7 +1706,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (player.getCurrentPreferredSubtitle() == null) { getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub -> context?.let { ctx -> - if (setSubtitles(sub)) { + if (setSubtitles(sub, false)) { player.saveData() player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index d9e8963e49b..ba32a19e532 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -11,7 +11,10 @@ import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle +import com.lagradost.cloudstream3.utils.SubtitleHelper +import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.UIHelper.toPx +import me.xdrop.fuzzywuzzy.FuzzySearch enum class SubtitleStatus { IS_ACTIVE, @@ -47,6 +50,56 @@ data class SubtitleData( else "$url|$name" } + /** Returns true if langCode is the same as the IETF tag */ + fun matchesLanguage(langCode: String): Boolean { + return getIETF_tag() == langCode + } + + /** Tries hard to figure out a valid IETF tag based on language code and name. Will return null if not found. */ + fun getIETF_tag(): String? { + val tag = fromCodeToLangTagIETF(this.languageCode) + if (tag != null) { + return tag + } + + // Remove any numbers to make matching better + val cleanedLanguage = originalName.replace(Regex("[0-9]"), "").trim() + + // First go for exact matches + SubtitleHelper.languages.forEach { language -> + if (language.languageName.equals(cleanedLanguage, ignoreCase = true) || + language.nativeName.equals(cleanedLanguage, ignoreCase = true) || + // Also match exact IETF tags + language.IETF_tag.equals(cleanedLanguage, ignoreCase = true) + ) { + return language.IETF_tag + } + } + + var closestMatch: Pair = null to 0 + // Then go for partial matches, however only use the best match + SubtitleHelper.languages.forEach { language -> + val lowerCleaned = cleanedLanguage.lowercase() + val score = maxOf( + FuzzySearch.ratio(lowerCleaned, language.languageName.lowercase()), + FuzzySearch.ratio( + lowerCleaned, language.nativeName.lowercase() + ) + ) + + // Arbitrary cutoff at 80. + if (cleanedLanguage.contains(language.languageName, ignoreCase = true) || + cleanedLanguage.contains(language.nativeName, ignoreCase = true) || score > 80 + ) { + if (score > closestMatch.second) { + closestMatch = language.IETF_tag to score + } + } + } + + return closestMatch.first + } + val name = "$originalName $nameSuffix" /** diff --git a/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt b/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt new file mode 100644 index 00000000000..b0233f58d5f --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt @@ -0,0 +1,120 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.player.SubtitleOrigin +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +/** Ensure partial subtitle language finding is reliable. */ +class SubtitleLanguageTagTest { + fun getQuickSubtitle(originalName: String, languageCode: String?): SubtitleData { + return SubtitleData( + originalName = originalName, + nameSuffix = "1", + url = "https://example.com/test.vtt", + origin = SubtitleOrigin.URL, + mimeType = "text/vtt", + headers = emptyMap(), + languageCode = languageCode + ) + } + + @Test + fun `returns languageCode directly if already valid IETF tag`() { + val subtitle = getQuickSubtitle( + originalName = "Anything", + languageCode = "en" + ) + + assertEquals("en", subtitle.getIETF_tag()) + } + + @Test + fun `matches exact language name`() { + val subtitle = getQuickSubtitle( + originalName = "English", + languageCode = null + ) + + assertEquals("en", subtitle.getIETF_tag()) + } + + @Test + fun `matches native language name`() { + val subtitle = getQuickSubtitle( + originalName = "Español", + languageCode = null + ) + + assertEquals("es", subtitle.getIETF_tag()) + } + + @Test + fun `matches fuzzy partial language name`() { + val subtitle = getQuickSubtitle( + originalName = "English [SUB]", + languageCode = null + ) + + assertEquals("en", subtitle.getIETF_tag()) + } + + @Test + fun `returns null when no language matches`() { + val subtitle = getQuickSubtitle( + originalName = "Klingon", + languageCode = null + ) + + assertNull(subtitle.getIETF_tag()) + } + + + @Test + fun `returns the correct language variant`() { + val subtitle1 = getQuickSubtitle( + originalName = "Chinese", + languageCode = null + ) + val subtitle2 = getQuickSubtitle( + originalName = "Chinese (subtitle)", + languageCode = null + ) + val subtitleSimplified1 = getQuickSubtitle( + originalName = "Chinese (simplified)", + languageCode = null + ) + val subtitleSimplified2 = getQuickSubtitle( + originalName = "Chinese - simplified", + languageCode = null + ) + val subtitleSimplified3 = getQuickSubtitle( + originalName = "Chinese simplified", + languageCode = "zh-" + ) + val subtitleSimplified4 = getQuickSubtitle( + originalName = "Chinese (simplified)2", + languageCode = "zh-hans" + ) + + assertEquals("zh", subtitle1.getIETF_tag()) + assertEquals("zh", subtitle2.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified1.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified2.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified3.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified4.getIETF_tag()) + } + + + @Test + fun `returns exact language matches`() { + val subtitle = getQuickSubtitle( + originalName = "en", + languageCode = null + ) + + assertEquals("en", subtitle.getIETF_tag()) + } +} + From f35d45bed9786a3846467339af7e63a3f4f4f943 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:02:00 +0000 Subject: [PATCH 2/3] Move logic to getLanguageDataFromName --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../ui/player/PlayerSubtitleHelper.kt | 48 +------------ .../cloudstream3/SubtitleSelectionTest.kt | 24 ++++++- .../cloudstream3/utils/SubtitleHelper.kt | 68 ++++++++++++++----- 4 files changed, 79 insertions(+), 67 deletions(-) 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 7d188e55d69..fde2f2484f4 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 @@ -1664,12 +1664,12 @@ class GeneratorPlayer : FullScreenPlayer() { ): SubtitleData? { val langCode = preferredAutoSelectSubtitles ?: return null if (downloads) { - return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguage(langCode) } + return sortSubs(subtitles).firstOrNull { it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode(langCode) } } if (!settings) return null - return sortSubs(subtitles).firstOrNull { it.matchesLanguage(langCode) } + return sortSubs(subtitles).firstOrNull { it.matchesLanguageCode(langCode) } } private fun autoSelectFromSettings(): Boolean { @@ -1679,7 +1679,7 @@ class GeneratorPlayer : FullScreenPlayer() { Log.i(TAG, "autoSelectFromSettings = $current") context?.let { ctx -> // Only use the player preferred subtitle if it matches the available language - if (current != null && (langCode == null || current.matchesLanguage(langCode))) { + if (current != null && (langCode == null || current.matchesLanguageCode(langCode))) { if (setSubtitles(current, false)) { player.saveData() player.reloadPlayer(ctx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index ba32a19e532..ee6170aa53f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -11,10 +11,8 @@ import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle -import com.lagradost.cloudstream3.utils.SubtitleHelper -import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF +import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.cloudstream3.utils.UIHelper.toPx -import me.xdrop.fuzzywuzzy.FuzzySearch enum class SubtitleStatus { IS_ACTIVE, @@ -51,53 +49,13 @@ data class SubtitleData( } /** Returns true if langCode is the same as the IETF tag */ - fun matchesLanguage(langCode: String): Boolean { + fun matchesLanguageCode(langCode: String): Boolean { return getIETF_tag() == langCode } /** Tries hard to figure out a valid IETF tag based on language code and name. Will return null if not found. */ fun getIETF_tag(): String? { - val tag = fromCodeToLangTagIETF(this.languageCode) - if (tag != null) { - return tag - } - - // Remove any numbers to make matching better - val cleanedLanguage = originalName.replace(Regex("[0-9]"), "").trim() - - // First go for exact matches - SubtitleHelper.languages.forEach { language -> - if (language.languageName.equals(cleanedLanguage, ignoreCase = true) || - language.nativeName.equals(cleanedLanguage, ignoreCase = true) || - // Also match exact IETF tags - language.IETF_tag.equals(cleanedLanguage, ignoreCase = true) - ) { - return language.IETF_tag - } - } - - var closestMatch: Pair = null to 0 - // Then go for partial matches, however only use the best match - SubtitleHelper.languages.forEach { language -> - val lowerCleaned = cleanedLanguage.lowercase() - val score = maxOf( - FuzzySearch.ratio(lowerCleaned, language.languageName.lowercase()), - FuzzySearch.ratio( - lowerCleaned, language.nativeName.lowercase() - ) - ) - - // Arbitrary cutoff at 80. - if (cleanedLanguage.contains(language.languageName, ignoreCase = true) || - cleanedLanguage.contains(language.nativeName, ignoreCase = true) || score > 80 - ) { - if (score > closestMatch.second) { - closestMatch = language.IETF_tag to score - } - } - } - - return closestMatch.first + return fromLanguageToTagIETF(this.languageCode) ?: fromLanguageToTagIETF(this.originalName, halfMatch = true) } val name = "$originalName $nameSuffix" diff --git a/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt b/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt index b0233f58d5f..93dc9dc0cd0 100644 --- a/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt +++ b/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt @@ -91,19 +91,28 @@ class SubtitleLanguageTagTest { ) val subtitleSimplified3 = getQuickSubtitle( originalName = "Chinese simplified", - languageCode = "zh-" + languageCode = "zhh" ) val subtitleSimplified4 = getQuickSubtitle( originalName = "Chinese (simplified)2", languageCode = "zh-hans" ) - + val subtitleSimplified5 = getQuickSubtitle( + originalName = "汉语", + languageCode = null + ) + val subtitleSimplified6 = getQuickSubtitle( + originalName = "", + languageCode = "zh-hans" + ) assertEquals("zh", subtitle1.getIETF_tag()) assertEquals("zh", subtitle2.getIETF_tag()) assertEquals("zh-hans", subtitleSimplified1.getIETF_tag()) assertEquals("zh-hans", subtitleSimplified2.getIETF_tag()) assertEquals("zh-hans", subtitleSimplified3.getIETF_tag()) assertEquals("zh-hans", subtitleSimplified4.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified5.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified6.getIETF_tag()) } @@ -116,5 +125,16 @@ class SubtitleLanguageTagTest { assertEquals("en", subtitle.getIETF_tag()) } + + + @Test + fun `returns partial language matches`() { + val subtitle = getQuickSubtitle( + originalName = "Englis", + languageCode = null + ) + + assertEquals("en", subtitle.getIETF_tag()) + } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt index cdfb6e9d710..8d5479cc0ab 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/SubtitleHelper.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.utils +import me.xdrop.fuzzywuzzy.FuzzySearch import java.util.Locale // If you find a way to use SettingsGeneral getCurrentLocale() @@ -37,7 +38,7 @@ object SubtitleHelper { * https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry * https://android.googlesource.com/platform/frameworks/base/+/android-16.0.0_r2/core/res/res/values/locale_config.xml * https://iso639-3.sil.org/code_tables/639/data/all - */ + */ data class LanguageMetadata( val languageName: String, val nativeName: String, @@ -75,32 +76,65 @@ object SubtitleHelper { /** * Language name (english or native) -> [LanguageMetadata] - * @param languageName language name - * @param halfMatch match with `contains()` instead of `equals()` - */ - private fun getLanguageDataFromName(languageName: String?, halfMatch: Boolean? = false): LanguageMetadata? { + * @param languageName language name or language tag + * @param halfMatch match with `contains()` instead of `equals()`. Also uses fuzzy matching to get approximate matches. + */ + private fun getLanguageDataFromName( + languageName: String?, + halfMatch: Boolean? = false + ): LanguageMetadata? { if (languageName.isNullOrBlank() || languageName.length < 2) return null // Workaround to avoid junk like "English (original audio)" or "Spanish 123" // or "اَلْعَرَبِيَّةُ (Original Audio) 1" or "English (hindi sub)"… + // Will still keep "-" to be compatible with language tags such as pr-bt val garbage = Regex( "\\([^)]*(?:dub|sub|original|audio|code)[^)]*\\)|" + // junk words in parenthesis - "[\\u064B-\\u065B]|" + // arabic diacritics - "\\d|" + // numbers - "[^\\p{L}\\p{Mn}\\p{Mc}\\p{Me} ()]" // non-letter (from any language) + "[\\u064B-\\u065B]|" + // arabic diacritics + "\\d|" + // numbers + "[^\\p{L}\\p{Mn}\\p{Mc}\\p{Me} ()-]" // non-letter (from any language) ) + + val lowLangName = languageName.lowercase().replace(garbage, "").trim() - val index = - indexMapLanguageName[lowLangName] ?: - indexMapNativeName[lowLangName] ?: -1 + + val index = indexMapLanguageName[lowLangName] + ?: indexMapNativeName[lowLangName] + ?: indexMapIETF_tag[lowLangName] + ?: -1 + val langMetadata = languages.getOrNull(index) - if (halfMatch == true && langMetadata == null) { - for (lang in languages) - if (lang.languageName.contains(lowLangName, ignoreCase = true) || - lang.nativeName.contains(lowLangName, ignoreCase = true)) - return lang + if (langMetadata != null) { + return langMetadata + } else if (halfMatch == true) { + // Go for partial matches but only use the best match + var closestMatch: Pair = null to 0 + + for (lang in languages) { + val score = maxOf( + FuzzySearch.ratio(lowLangName, lang.languageName.lowercase()), + FuzzySearch.ratio( + lowLangName, lang.nativeName.lowercase() + ) + ) + + // Usually the languageName or nativeName is a substring of the entered name, for example in "English Subtitle" + if (lowLangName.contains(lang.languageName, ignoreCase = true) || + lowLangName.contains(lang.nativeName, ignoreCase = true) || + // Arbitrary cutoff at 80. + score > 80 + ) { + // First detected language gets priority in equal scores. + if (score > closestMatch.second) { + closestMatch = lang to score + } + } + } + + return closestMatch.first } - return langMetadata + + return null } @Deprecated( From 472a56d5b3aa97b88a148c471aaf31634071f80e Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:50:01 +0000 Subject: [PATCH 3/3] Add source hiding Fix footer bug Update QualityDataHelper.kt rebase --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 110 ++++++++++++---- .../ui/player/PlayerGeneratorViewModel.kt | 62 +++++++-- .../source_priority/QualityDataHelper.kt | 50 +++++++- .../source_priority/SourcePriorityDialog.kt | 119 +++++++++--------- .../SourceProfileSettingsDialog.kt | 48 +++++++ .../player_select_source_priority.xml | 34 ++--- .../layout/player_select_source_priority.xml | 7 ++ .../layout/source_profile_settings_dialog.xml | 99 +++++++++++++++ app/src/main/res/values/strings.xml | 8 ++ 9 files changed, 426 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt create mode 100644 app/src/main/res/layout/source_profile_settings_dialog.xml 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 91e3ff9708d..683a322c466 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 @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap +import android.graphics.Typeface import android.os.Build import android.os.Bundle import android.text.Spanned @@ -79,6 +80,7 @@ import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioT 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.LinkSource +import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog @@ -132,6 +134,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.io.Serializable +import java.lang.ref.WeakReference import java.util.Calendar import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -510,7 +513,7 @@ class GeneratorPlayer : FullScreenPlayer() { showDownloadProgress(DownloadEvent(0, 0, 0, null)) // uiReset() // Removed due to UX - + currentSelectedLink = link // setEpisodes(viewModel.getAllMeta() ?: emptyList()) setPlayerDimen(null) @@ -1112,21 +1115,29 @@ class GeneratorPlayer : FullScreenPlayer() { var sourceIndex = 0 var startSource = 0 - var sortedUrls = emptyList>() + // Filtered and sorted links + var currentHiddenFooter: View? = null + var filteredLinks: List = emptyList() fun refreshLinks(qualityProfile: Int) { - sortedUrls = viewModel.state.sortLinks(qualityProfile) - if (sortedUrls.isEmpty()) { + val currentLinkUsed = currentSelectedLink + // Always display current linkFooter + val sortedLinks = viewModel.state.sortLinks(qualityProfile) + + filteredLinks = sortedLinks.filter { it.shouldUseLink || it.link == currentLinkUsed } + + if (sortedLinks.isEmpty()) { sourceDialog.findViewById(R.id.sort_sources_holder)?.isGone = true } else { - startSource = sortedUrls.indexOf(currentSelectedLink) + startSource = filteredLinks.indexOfFirst { it.link == currentLinkUsed } sourceIndex = startSource val sourcesArrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) -> + sourcesArrayAdapter.addAll(filteredLinks.map { displayLink -> + val (link, uri) = displayLink.link val name = link?.name ?: uri?.name ?: "NULL" "$name ${Qualities.getStringByInt(link?.quality)}" }) @@ -1142,7 +1153,7 @@ class GeneratorPlayer : FullScreenPlayer() { } providerList.setOnItemLongClickListener { _, _, position, _ -> - sortedUrls.getOrNull(position)?.first?.url?.let { + sortedLinks.getOrNull(position)?.link?.first?.url?.let { clipboardHelper( txt(R.string.video_source), it @@ -1150,6 +1161,25 @@ class GeneratorPlayer : FullScreenPlayer() { } true } + + val hiddenLinks = sortedLinks.size - filteredLinks.size + providerList.removeFooterView(currentHiddenFooter) + + if (hiddenLinks > 0) { + val hiddenLinksFooter: TextView = layoutInflater.inflate( + R.layout.sort_bottom_footer_add_choice, null + ) as TextView + + providerList.addFooterView(hiddenLinksFooter, null, false) + currentHiddenFooter = hiddenLinksFooter + + val hiddenLinksText = + ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) + .format(hiddenLinks) + hiddenLinksFooter.text = hiddenLinksText + hiddenLinksFooter.setCompoundDrawables(null, null, null, null) + hiddenLinksFooter.setTypeface(null, Typeface.ITALIC) + } } } @@ -1363,8 +1393,8 @@ class GeneratorPlayer : FullScreenPlayer() { } } if (init) { - sortedUrls.getOrNull(sourceIndex)?.let { - loadLink(it, true) + filteredLinks.getOrNull(sourceIndex)?.let { + loadLink(it.link, true) } } sourceDialog.dismissSafe(activity) @@ -1532,6 +1562,10 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerError(exception: Throwable) { + currentSelectedLink?.let { link -> + viewModel.modifyState { this.addError(link) } + } + val currentUrl = currentSelectedLink?.let { it.first?.url ?: it.second?.uri?.toString() } ?: "unknown" val headers = currentSelectedLink?.first?.headers?.toString() ?: "none" @@ -1554,8 +1588,22 @@ class GeneratorPlayer : FullScreenPlayer() { private fun noLinksFound() { viewModel.forceClearCache = true + val hiddenLinks = viewModel.state.sortLinks(currentQualityProfile).count { !it.shouldUseLink } + + context?.let { ctx -> + // Display that there are hidden links to the user. + if (hiddenLinks > 0) { + val noLinksString = ctx.getString(R.string.no_links_found_toast) + val hiddenString = + ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks) + .format(hiddenLinks) + val toastText = "$noLinksString\n($hiddenString)" + showToast(toastText, Toast.LENGTH_SHORT) + } else { + showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) + } + } - showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) activity?.popCurrentPage() } @@ -1566,7 +1614,9 @@ class GeneratorPlayer : FullScreenPlayer() { } val links = viewModel.state.sortLinks(currentQualityProfile) - if (links.isEmpty()) { + + val firstAvailableLink = links.firstOrNull { it.shouldUseLink }?.link + if (firstAvailableLink == null) { noLinksFound() return } @@ -1574,7 +1624,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (!isPlayerActive.compareAndSet(false, true)) { return } - loadLink(links.first(), false) + loadLink(firstAvailableLink, false) showPlayerMetadata() } @@ -1641,25 +1691,26 @@ class GeneratorPlayer : FullScreenPlayer() { } } - override fun hasNextMirror(): Boolean { + private fun getNextLink(): DisplayLink? { val links = viewModel.state.sortLinks(currentQualityProfile) - return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size + val currentIndex = links.indexOfFirst { it.link == currentSelectedLink } + val nextPotentialLink = + links.withIndex().firstOrNull { it.index > currentIndex && it.value.shouldUseLink } + return nextPotentialLink?.value } - override fun nextMirror() { - val links = viewModel.state.sortLinks(currentQualityProfile) - if (links.isEmpty()) { - noLinksFound() - return - } + override fun hasNextMirror(): Boolean { + return getNextLink() != null + } - val newIndex = links.indexOf(currentSelectedLink) + 1 - if (newIndex >= links.size) { + override fun nextMirror() { + val nextLink = getNextLink() + if (nextLink == null) { noLinksFound() return } - loadLink(links[newIndex], true) + loadLink(nextLink.link, true) } override fun onDestroy() { @@ -2166,6 +2217,7 @@ class GeneratorPlayer : FullScreenPlayer() { isPlayerActive.set(false) binding?.overlayLoadingSkipButton?.isVisible = false binding?.playerLoadingOverlay?.isVisible = true + viewModel.modifyState { setError(emptyList()) } uiReset() } @@ -2297,19 +2349,23 @@ class GeneratorPlayer : FullScreenPlayer() { } } - observe(viewModel.currentLinks) { (links, instance) -> + observe(viewModel.currentLinks) { (_, instance) -> if (instance != viewModel.state.instance) return@observe // Outdated observe - val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true + val sortedLinks = viewModel.state.sortLinks(currentQualityProfile) + val usableLinks = sortedLinks.count { link -> link.shouldUseLink } + + val turnVisible = usableLinks > 0 && viewModel.generator?.canSkipLoading == true val wasGone = binding.overlayLoadingSkipButton.isGone binding.overlayLoadingSkipButton.apply { isVisible = turnVisible - if (links.isEmpty()) { + + if (usableLinks == 0) { setText(R.string.skip_loading) } else { @SuppressLint("SetTextI18n") - text = "${context.getString(R.string.skip_loading)} (${links.size})" + text = "${context.getString(R.string.skip_loading)} (${usableLinks})" } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index e3c390d504c..cb8cf8bfff5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -10,6 +10,8 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings +import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -40,12 +42,20 @@ data class GeneratorState( val id: Int?, ) +data class DisplayLink( + val link: VideoLink, + // If the link should be displayed and used by the player + val shouldUseLink: Boolean, + val priority: Int +) + /** Immutable state of all current links relevant to displaying the video */ // @MustUseReturnValues // @Immutable data class VideoState( val subtitles: PersistentSet = persistentSetOf(), val links: PersistentSet = persistentSetOf(), + val erroredLinks: PersistentSet = persistentSetOf(), val stamps: PersistentList = persistentListOf(), val loading: Resource = Resource.Loading(), val generatorState: GeneratorState? = null, @@ -56,19 +66,52 @@ data class VideoState( * * sortedBy is not exactly expensive, but each hasNextMirror does it again, so this alleviates unnecessary recomputation * */ - private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() + private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() + /** + * The cache is guaranteed to be up to date link-wise due to the immutable links. + * However, hideNegativeSources and hideErrorSources could be updated, which requires clearing the cache. + */ fun clearSortedLinksCache() = sortedLinks.clear() + private fun hasLinkErrored(link: VideoLink): Boolean { + return erroredLinks.any { it == link } + } + + private fun VideoLink.toDisplayLink( + qualityProfile: Int, + hideNegativeSources: Boolean, + hideErrorSources: Boolean + ): DisplayLink { + val priority = getLinkPriority(qualityProfile, this.first) + val shouldHideLink = + (hideNegativeSources && priority < 0) || (hideErrorSources && hasLinkErrored(this)) + val displayLink = DisplayLink(this, !shouldHideLink, priority) + + return displayLink + } + // Modifying sortedLinks is not considered a "visible" side effect, and rerunning it does not change the result // It is by all standards, idempotent and by extension also pure as it has no "visible" side effect /** Returns .links in the sorted order according to the qualityProfile. * Use .links if order is not needed */ @Contract(pure = true) - fun sortLinks(qualityProfile: Int): List { - return sortedLinks[qualityProfile] ?: links.sortedBy { link -> + fun sortLinks(qualityProfile: Int): List { + sortedLinks[qualityProfile]?.let { + return it + } + + val hideNegativeSources = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideNegativeSources) + val hideErrorSources = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources) + + return links.map { link -> + // negative because we want to sort highest quality first + link.toDisplayLink(qualityProfile, hideNegativeSources, hideErrorSources) + }.sortedBy { // negative because we want to sort highest quality first - -getLinkPriority(qualityProfile, link.first) + -it.priority }.also { value -> sortedLinks[qualityProfile] = value } } @@ -113,6 +156,12 @@ data class VideoState( @JvmName("setVideoSkipStamp") @Contract(pure = true) fun set(items: Collection): VideoState = copy(stamps = items.toPersistentList()) + + @Contract(pure = true) + fun addError(item: VideoLink): VideoState = copy(erroredLinks = erroredLinks.add(item)) + + @Contract(pure = true) + fun setError(items: Collection): VideoState = copy(erroredLinks = items.toPersistentSet()) } data class VideoLive( @@ -141,9 +190,8 @@ class PlayerGeneratorViewModel : ViewModel() { var state = VideoState(instance = 0) private set - private val _currentLinks = - MutableLiveData>>>(null) - val currentLinks: LiveData>>> = _currentLinks + private val _currentLinks = MutableLiveData>>(null) + val currentLinks: LiveData>> = _currentLinks private val _currentSubtitles = MutableLiveData>>(null) val currentSubtitles: LiveData>> = _currentSubtitles 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 index 02470484ea1..bc110c183dd 100644 --- 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 @@ -12,12 +12,16 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities +import java.util.EnumMap +import java.util.concurrent.ConcurrentHashMap +import kotlin.also import kotlin.math.abs 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" + const val VIDEO_PROFILE_SETTINGS = "video_profile_settings" // Old key only supporting one type per profile @Deprecated("Changed to support multiple types per profile") @@ -53,13 +57,21 @@ object QualityDataHelper { val types: Set ) + + // Map profile and name to priority + val sourcePriorityCache: ConcurrentHashMap> = ConcurrentHashMap() + fun getSourcePriority(profile: Int, name: String?): Int { if (name == null) return DEFAULT_SOURCE_PRIORITY - return getKey( + + return sourcePriorityCache[profile]?.get(name) ?: (getKey( "$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile", name, DEFAULT_SOURCE_PRIORITY - ) ?: DEFAULT_SOURCE_PRIORITY + ) ?: DEFAULT_SOURCE_PRIORITY).also { + sourcePriorityCache.getOrPut(profile) { hashMapOf() } + sourcePriorityCache[profile]?.set(name, it) + } } fun getAllSourcePriorityNames(profile: Int): List { @@ -77,6 +89,8 @@ object QualityDataHelper { } else { setKey(folder, name, priority) } + + sourcePriorityCache[profile]?.set(name, priority) } fun setProfileName(profile: Int, name: String?) { @@ -93,12 +107,17 @@ object QualityDataHelper { ?: txt(R.string.profile_number, profile) } + // Map profile and quality to priority + val qualityPriorityCache: ConcurrentHashMap> = ConcurrentHashMap() fun getQualityPriority(profile: Int, quality: Qualities): Int { - return getKey( + return qualityPriorityCache[profile]?.get(quality) ?: (getKey( "$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile", quality.value.toString(), quality.defaultPriority - ) ?: quality.defaultPriority + )?.also { + qualityPriorityCache.getOrPut(profile) { EnumMap(Qualities::class.java) } + qualityPriorityCache[profile]?.set(quality, it) + }) ?: quality.defaultPriority } fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) { @@ -107,8 +126,24 @@ object QualityDataHelper { quality.value.toString(), priority ) + qualityPriorityCache[profile]?.set(quality, priority) + } + + fun setProfileSetting(profile: Int, setting: ProfileSettings, value: T) { + val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" + // Prevent unnecessary keys + if (value == setting.defaultValue) { + removeKey(folder, setting.key) + } else { + setKey(folder, setting.key, value) + } } + inline fun getProfileSetting(profile: Int, setting: ProfileSettings): T { + val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" + val value = getKey(folder, setting.key) + return value ?: setting.defaultValue + } @Suppress("DEPRECATION") fun getQualityProfileTypes(profile: Int): Set { @@ -223,4 +258,9 @@ object QualityDataHelper { if (target == null) return Qualities.Unknown return Qualities.entries.minBy { abs(it.value - target) } } -} \ No newline at end of file +} + +sealed class ProfileSettings(val key: String, val defaultValue: T) { + object HideErrorSources : ProfileSettings("hide_error_sources", false) + object HideNegativeSources : ProfileSettings("hide_negative_sources", false) +} 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 index c8ac96ebbf6..604ec314921 100644 --- 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 @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding class SourcePriorityDialog( val ctx: Context, - @StyleRes themeRes: Int, + @StyleRes val themeRes: Int, val links: List, private val profile: QualityDataHelper.QualityProfile, /** @@ -28,76 +28,79 @@ class SourcePriorityDialog( PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false) setContentView(binding.root) fixSystemBarsPadding(binding.root) - val sourcesRecyclerView = binding.sortSources - val qualitiesRecyclerView = binding.sortQualities - val profileText = binding.profileTextEditable - val saveBtt = binding.saveBtt - val exitBtt = binding.closeBtt - val helpBtt = binding.helpBtt - profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) - profileText.hint = txt(R.string.profile_number, profile.id).asString(context) + binding.apply { + profileTextEditable.setText( + QualityDataHelper.getProfileName(profile.id).asString(context) + ) + profileTextEditable.hint = txt(R.string.profile_number, profile.id).asString(context) - sourcesRecyclerView.adapter = PriorityAdapter( - ).apply { - submitList(links.map { link -> - SourcePriority( - null, - link.source, - QualityDataHelper.getSourcePriority(profile.id, link.source) - ) - }.distinctBy { it.name }.sortedBy { -it.priority }) - } + sortSources.adapter = PriorityAdapter( + ).apply { + val sortedLinks = links.map { link -> + SourcePriority( + null, + link.source, + QualityDataHelper.getSourcePriority(profile.id, link.source) + ) + }.distinctBy { it.name }.sortedBy { -it.priority } - qualitiesRecyclerView.adapter = PriorityAdapter( - ).apply { - submitList(Qualities.entries.mapNotNull { - SourcePriority( - it, - Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, - QualityDataHelper.getQualityPriority(profile.id, it) - ) - }.sortedBy { -it.priority }) - } + submitList(sortedLinks) + } - @Suppress("UNCHECKED_CAST") // We know the types - saveBtt.setOnClickListener { - val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter - val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter + sortQualities.adapter = PriorityAdapter( + ).apply { + submitList(Qualities.entries.mapNotNull { + SourcePriority( + it, + Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, + QualityDataHelper.getQualityPriority(profile.id, it) + ) + }.sortedBy { -it.priority }) + } - val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() - val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() + @Suppress("UNCHECKED_CAST") // We know the types + saveBtt.setOnClickListener { + val qualityAdapter = sortQualities.adapter as? PriorityAdapter + val sourcesAdapter = sortSources.adapter as? PriorityAdapter - qualities.forEach { - QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) - } + val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() + val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() - sources.forEach { - QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) - } + qualities.forEach { + QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) + } + + sources.forEach { + QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) + } - qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) - sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) + qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) + sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) - val savedProfileName = profileText.text.toString() - if (savedProfileName.isBlank()) { - QualityDataHelper.setProfileName(profile.id, null) - } else { - QualityDataHelper.setProfileName(profile.id, savedProfileName) + val savedProfileName = profileTextEditable.text.toString() + if (savedProfileName.isBlank()) { + QualityDataHelper.setProfileName(profile.id, null) + } else { + QualityDataHelper.setProfileName(profile.id, savedProfileName) + } + updatedCallback.invoke() } - updatedCallback.invoke() - } - exitBtt.setOnClickListener { - this.dismissSafe() - } + closeBtt.setOnClickListener { + dismissSafe() + } - helpBtt.setOnClickListener { - AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setMessage(R.string.quality_profile_help) - }.show() - } + helpBtt.setOnClickListener { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setMessage(R.string.quality_profile_help) + }.show() + } + settingsBtt.setOnClickListener { + SourceProfileSettingsDialog(ctx, themeRes, profile.id).show() + } + } super.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt new file mode 100644 index 00000000000..c26955b5efe --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt @@ -0,0 +1,48 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.app.Dialog +import android.content.Context +import android.view.LayoutInflater +import androidx.annotation.StyleRes +import com.lagradost.cloudstream3.databinding.SourceProfileSettingsDialogBinding +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding + +class SourceProfileSettingsDialog( + val ctx: Context, + @StyleRes themeRes: Int, + val profile: Int +) : Dialog(ctx, themeRes) { + override fun show() { + val binding = + SourceProfileSettingsDialogBinding.inflate(LayoutInflater.from(ctx), null, false) + setContentView(binding.root) + fixSystemBarsPadding(binding.root) + + binding.apply { + var hideErrorSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideErrorSources) + var hideNegativeSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideNegativeSources) + + profileHideErrorSources.isChecked = hideErrorSources + profileHideErrorSources.setOnCheckedChangeListener { _, bool -> + hideErrorSources = bool + } + + profileHideNegativeSources.isChecked = hideNegativeSources + profileHideNegativeSources.setOnCheckedChangeListener { _, bool -> + hideNegativeSources = bool + } + + applyBtt.setOnClickListener { + QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideErrorSources, hideErrorSources) + QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideNegativeSources, hideNegativeSources) + dismissSafe() + } + + cancelBtt.setOnClickListener { + dismissSafe() + } + } + super.show() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-port/player_select_source_priority.xml b/app/src/main/res/layout-port/player_select_source_priority.xml index 2cba9c869bb..926227c58e9 100644 --- a/app/src/main/res/layout-port/player_select_source_priority.xml +++ b/app/src/main/res/layout-port/player_select_source_priority.xml @@ -1,6 +1,5 @@ - - + @@ -33,14 +32,21 @@ android:textSize="20sp" android:textStyle="bold" /> + + + android:src="@drawable/baseline_help_outline_24" /> - + + tools:ignore="LabelFor" + tools:text="@string/profile_number" /> + android:text="@string/sort_save" /> + android:text="@string/sort_close" /> diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 182cd186141..0d825de77e8 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -160,6 +160,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31cf951cf5f..14780da6bda 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -622,6 +622,14 @@ Subscribe Unsubscribe Profile %d + Profile settings + Hide sources with a negative priority + Hide sources with errors + + %d hidden link + %d hidden links + + Wi-Fi Mobile data Set default