From 1075ded170a44f353e8236e903b5a096ab3f82f8 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 17 Dec 2025 15:32:37 +0100 Subject: [PATCH 1/4] Language for video support, original for video support, deduplication fix for languages on videos, submods --- app/src/main/assets/scripts/source.js | 7 +++++ .../streams/sources/DashManifestSource.kt | 3 ++ .../streams/sources/HLSManifestSource.kt | 3 ++ .../streams/sources/HLSVariantUrlSource.kt | 3 ++ .../models/streams/sources/IVideoSource.kt | 2 ++ .../streams/sources/LocalVideoSource.kt | 4 +++ .../models/streams/sources/VideoUrlSource.kt | 3 ++ .../models/sources/JSDashManifestRawSource.kt | 7 +++++ .../js/models/sources/JSDashManifestSource.kt | 6 ++++ .../sources/JSDashManifestWidevineSource.kt | 6 ++++ .../js/models/sources/JSHLSManifestSource.kt | 6 ++++ .../js/models/sources/JSVideoUrlSource.kt | 3 ++ .../models/sources/LocalVideoContentSource.kt | 3 ++ .../models/sources/LocalVideoFileSource.kt | 3 ++ .../mainactivity/main/VideoDetailView.kt | 14 ++++++++-- .../platformplayer/helpers/VideoHelper.kt | 28 +++++++++++++++++-- 16 files changed, 95 insertions(+), 6 deletions(-) diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js index 821fa656..03d9c810 100644 --- a/app/src/main/assets/scripts/source.js +++ b/app/src/main/assets/scripts/source.js @@ -415,6 +415,8 @@ class VideoUrlSource { this.url = obj.url; if(obj.requestModifier) this.requestModifier = obj.requestModifier; + this.language = obj?.language; + this.original = obj?.original; } } class VideoUrlWidevineSource extends VideoUrlSource { @@ -512,6 +514,8 @@ class HLSSource { this.language = obj.language; if(obj.requestModifier) this.requestModifier = obj.requestModifier; + this.language = obj?.language; + this.original = obj?.original; } } class DashSource { @@ -525,6 +529,8 @@ class DashSource { this.language = obj.language; if(obj.requestModifier) this.requestModifier = obj.requestModifier; + this.language = obj?.language; + this.original = obj?.original; } } class DashWidevineSource extends DashSource { @@ -550,6 +556,7 @@ class DashManifestRawSource { this.language = obj.language ?? Language.UNKNOWN; if(obj.requestModifier) this.requestModifier = obj.requestModifier; + this.original = obj?.original; } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/DashManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/DashManifestSource.kt index 3d117516..fa23ac28 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/DashManifestSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/DashManifestSource.kt @@ -12,6 +12,9 @@ class DashManifestSource : IVideoSource, IDashManifestSource { override var priority: Boolean = false; + override val language: String? = null; + override val original: Boolean? = false; + constructor(url : String) { this.url = url; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSManifestSource.kt index 52304473..5f0c94a6 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSManifestSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSManifestSource.kt @@ -12,6 +12,9 @@ class HLSManifestSource : IVideoSource, IHLSManifestSource { override var priority: Boolean = false; + override val language: String? = null; + override val original: Boolean? = false; + constructor(url : String) { this.url = url; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSVariantUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSVariantUrlSource.kt index 858c53ca..12be8b16 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSVariantUrlSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSVariantUrlSource.kt @@ -14,6 +14,9 @@ class HLSVariantVideoUrlSource( override val priority: Boolean, val url: String ) : IVideoUrlSource { + override val language: String? = null; + override val original: Boolean? = false; + override fun getVideoUrl(): String { return url } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IVideoSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IVideoSource.kt index 867c1ee5..059bafde 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IVideoSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/IVideoSource.kt @@ -9,4 +9,6 @@ interface IVideoSource { val bitrate : Int?; val duration: Long; val priority: Boolean; + val language: String?; + val original: Boolean?; } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/LocalVideoSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/LocalVideoSource.kt index 5d15ddb8..ae72e823 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/LocalVideoSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/LocalVideoSource.kt @@ -16,6 +16,10 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource { override var priority: Boolean = false; + override val language: String? = null; + override val original: Boolean? = false; + + val filePath : String; val fileSize : Long; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/VideoUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/VideoUrlSource.kt index ebc112ec..21c69a04 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/VideoUrlSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/VideoUrlSource.kt @@ -19,6 +19,9 @@ open class VideoUrlSource( ) : IVideoUrlSource, IStreamMetaDataSource { override var streamMetaData: StreamMetaData? = null; + override val language: String? = null; + override val original: Boolean? = false; + override fun getVideoUrl() : String { return url; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt index b9c08db9..463a43f9 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt @@ -39,6 +39,10 @@ open class JSDashManifestRawSource( private val ctx = "DashRawSource" private val cfg = plugin.config + override val language: String? = _obj.getOrDefault(cfg, "language", ctx, null); + override val original: Boolean? = _obj.getOrDefault(cfg, "original", ctx, null); + + override val container: String = _obj.getOrDefault(cfg, "container", ctx, null) ?: "application/dash+xml" @@ -185,6 +189,9 @@ class JSDashManifestMergingRawSource( override val priority: Boolean get() = video.priority; + override val language: String? get() = audio.language + override val original: Boolean? get() = audio.original; + override fun generateAsync(scope: CoroutineScope): V8Deferred { val videoDashDef = video.generateAsync(scope); val audioDashDef = audio.generateAsync(scope); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt index 3070a2d4..8d26b689 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestSource.kt @@ -21,6 +21,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource { override var priority: Boolean = false; + override val language: String?; + override val original: Boolean?; + constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) { val contextName = "DashSource"; val config = plugin.config; @@ -29,6 +32,9 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource { duration = _obj.getOrThrow(config, "duration", contextName); priority = obj.getOrNull(config, "priority", contextName) ?: false; + + language = obj.getOrNull(config, "language", contextName); + original = obj.getOrNull(config, "original", contextName); } override fun getVideoUrl(): String { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt index 7700bd82..f6ce30f3 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt @@ -28,6 +28,9 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource, override val licenseUri: String override val hasLicenseRequestExecutor: Boolean + override val language: String?; + override val original: Boolean?; + @Suppress("ConvertSecondaryConstructorToPrimary") constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) { val contextName = "DashWidevineSource" @@ -40,6 +43,9 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource, licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") + + language = _obj.getOrNull(config, "language", contextName); + original = _obj.getOrNull(config, "original", contextName); } override fun getLicenseRequestExecutor(): JSRequestExecutor? { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt index 606d107c..1e003dcb 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestSource.kt @@ -21,6 +21,9 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource { override var priority: Boolean = false; + override val language: String?; + override val original: Boolean?; + constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) { val contextName = "HLSSource"; val config = plugin.config; @@ -30,5 +33,8 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource { duration = _obj.getOrThrow(config, "duration", contextName).toLong(); priority = obj.getOrNull(config, "priority", contextName) ?: false; + + language = _obj.getOrNull(config, "language", contextName); + original = _obj.getOrNull(config, "original", contextName); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt index dbf5242a..da514d78 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlSource.kt @@ -44,6 +44,9 @@ open class JSVideoUrlSource( override var priority: Boolean = _obj.getOrDefault(cfg, "priority", ctx, false) ?: false + override val language: String? = _obj.getOrDefault(cfg, "language", ctx, null); + override val original: Boolean? = _obj.getOrDefault(cfg, "original", ctx, null); + override fun getVideoUrl(): String = url override fun toString(): String = diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt index e8b37364..e09f7d5a 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoContentSource.kt @@ -20,6 +20,9 @@ class LocalVideoContentSource: IVideoSource { override val duration: Long; override val priority: Boolean = false; + override val language: String? = null; + override val original: Boolean? = false; + var contentUrl: String; constructor(contentUrl: String, mime: String, name: String? = null, duration: Long = 0) { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt index 4b4ff583..7378748f 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/local/models/sources/LocalVideoFileSource.kt @@ -20,6 +20,9 @@ class LocalVideoFileSource: IVideoSource { override val duration: Long; override val priority: Boolean = false; + override val language: String? = null; + override val original: Boolean? = null; + var file: File; constructor(file: File) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 9eae9587..9266df66 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -2423,9 +2423,17 @@ class VideoDetailView : ConstraintLayout { val doDedup = Settings.instance.playback.simplifySources; - val bestVideoSources = if(doDedup) (videoSources?.map { it.height * it.width } - ?.distinct() - ?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) } + val allLanguages = videoSources?.map { it.language } ?: listOf(); + val langResCombinations = if(videoSources != null) allLanguages.flatMap { + lang -> videoSources + .filter { v -> v.language == lang } + .map { it.height * it.width } + .distinct() + .map { res -> Pair(res, lang) } + } else listOf(); + + val bestVideoSources = if(doDedup && videoSources != null) (langResCombinations + ?.map { comb -> VideoHelper.selectBestVideoSource(videoSources.filter { comb.first == it.height * it.width && comb.second == it.language }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) } ?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource })) ?.distinct() ?.filterNotNull() diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 9c65221e..1947736d 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -52,8 +52,8 @@ class VideoHelper { fun isDownloadable(source: IVideoSource) = (source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource) && source !is IWidevineSource fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && source !is IWidevineSource - fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers); - fun selectBestVideoSource(sources: Iterable, desiredPixelCount : Int, prefContainers : Array) : IVideoSource? { + fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array, preferredLanguage: String? = null) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers, preferredLanguage); + fun selectBestVideoSource(sources: Iterable, desiredPixelCount : Int, prefContainers : Array, preferredLanguage: String? = null) : IVideoSource? { val targetVideo = if(desiredPixelCount > 0) { sources.toList().minByOrNull { x -> abs(x.height * x.width - desiredPixelCount) }; } else { @@ -63,12 +63,34 @@ class VideoHelper { val hasPriority = sources.any { it.priority }; val targetPixelCount = if(targetVideo != null) targetVideo.width * targetVideo.height else desiredPixelCount; - val altSources = if(hasPriority) { + + //Filter priority + var altSources = if(hasPriority) { sources.filter { it.priority }.sortedBy { x -> abs(x.height * x.width - targetPixelCount) }; } else { sources.filter { it.height == (targetVideo?.height ?: 0) }; } + //Filter Original + val hasOriginal = altSources.any { it.original == true }; + if(hasOriginal && Settings.instance.playback.preferOriginalAudio) + altSources = altSources.filter { it.original == true }; + + //Filter Language + val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) { + preferredLanguage + } else { + if(altSources.any { it.language == Language.ENGLISH }) + Language.ENGLISH; + else + Language.UNKNOWN; + } + if(altSources.any { it.language == languageToFilter }) { + altSources.filter { it.language == languageToFilter }.sortedBy { it.bitrate }.toList(); + } else { + altSources.sortedBy { it.bitrate } + } + var bestSource = altSources.firstOrNull(); for (prefContainer in prefContainers) { val betterSource = altSources.firstOrNull { it.container == prefContainer }; From 34d2e62314e3c6194c7a260bef4ae401f5038c37 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 17 Dec 2025 16:27:12 +0100 Subject: [PATCH 2/4] sub mods --- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 5e903fa5..079dc6e3 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 5e903fa569247b8c7483785ff0e25e2c3abc293a +Subproject commit 079dc6e3dca7aff63d3b693fddcce89fe373b3b2 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 5e903fa5..079dc6e3 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 5e903fa569247b8c7483785ff0e25e2c3abc293a +Subproject commit 079dc6e3dca7aff63d3b693fddcce89fe373b3b2 From 0fbe0bb438f295ecd51749024df13352ae3129f7 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 17 Dec 2025 19:43:56 +0100 Subject: [PATCH 3/4] Add filters for video languages to resolve excessive sources --- .../java/com/futo/platformplayer/Settings.kt | 2 +- .../mainactivity/main/VideoDetailView.kt | 55 +++++++++++++++++-- .../overlays/slideup/SlideUpMenuButtonList.kt | 36 ++++++++++-- ...y_slide_up_menu_button_list_scrollable.xml | 16 ++++++ 4 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/layout/overlay_slide_up_menu_button_list_scrollable.xml diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index b01e6a94..f5796d4d 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -387,7 +387,7 @@ class Settings : FragmentedStorageFileJson() { @DropdownFieldOptionsId(R.array.audio_languages) var primaryLanguage: Int = 0; - fun getPrimaryLanguage(context: Context): String? { + fun getPrimaryLanguage(context: Context? = null): String? { return when(primaryLanguage) { 0 -> "en"; 1 -> "es"; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 9266df66..921ec92c 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -33,6 +33,7 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.compose.ui.text.toLowerCase import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.lifecycleScope import androidx.media3.common.C @@ -2423,7 +2424,7 @@ class VideoDetailView : ConstraintLayout { val doDedup = Settings.instance.playback.simplifySources; - val allLanguages = videoSources?.map { it.language } ?: listOf(); + val allLanguages = videoSources?.map { it.language }?.distinct() ?: listOf(); val langResCombinations = if(videoSources != null) allLanguages.flatMap { lang -> videoSources .filter { v -> v.language == lang } @@ -2432,6 +2433,43 @@ class VideoDetailView : ConstraintLayout { .map { res -> Pair(res, lang) } } else listOf(); + + Log.i(TAG, "Language count: ${allLanguages}"); + var videoSourceItems = mutableListOf(); + var selectedLanguage: String? = null; + val languageFilters = if(allLanguages.filter { it != null }.count() > 1) + SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { + var languageFilterLabels = allLanguages.filterNotNull().toList(); + val english = languageFilterLabels.find { it?.lowercase() == "en" }; + val originalLanguage = videoSources?.find { it.original == true }?.language; + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); + val hasPrimaryLanguage = videoSources?.any { it.language == primaryLanguage } ?: false; + + if(english != null) + languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList(); + if(primaryLanguage != null && languageFilterLabels.contains(primaryLanguage)) + languageFilterLabels = listOf(primaryLanguage).plus(languageFilterLabels.filter { it != primaryLanguage }).toList(); + if(originalLanguage != null) + languageFilterLabels = listOf(originalLanguage).plus(languageFilterLabels.filter { it != originalLanguage }).toList(); + Log.i(TAG, "Language filtesr: ${languageFilterLabels.joinToString(", ")}"); + selectedLanguage = originalLanguage ?: (if(hasPrimaryLanguage) primaryLanguage else null); + setButtons(languageFilterLabels, selectedLanguage); + onClick.subscribe { selected -> + setSelected(selected); + + videoSourceItems.forEach { + val item = it.itemTag; + if(item is IVideoSource) { + if(item.language == selected) + it.visibility = View.VISIBLE; + else + it.visibility = View.GONE; + } + } + } + } + else null; + val bestVideoSources = if(doDedup && videoSources != null) (langResCombinations ?.map { comb -> VideoHelper.selectBestVideoSource(videoSources.filter { comb.first == it.height * it.width && comb.second == it.language }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) } ?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource })) @@ -2539,11 +2577,10 @@ class VideoDetailView : ConstraintLayout { call = { _player.selectAudioTrack(it.bitrate) }); }.toList().toTypedArray()) else null, - + if(languageFilters != null) languageFilters else null, if(bestVideoSources.isNotEmpty()) SlideUpMenuGroup(this.context, context.getString(R.string.video), "video", - *bestVideoSources - .map { + (bestVideoSources.map { val estSize = VideoHelper.estimateSourceSize(it); val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; SlideUpMenuItem(this.context, @@ -2552,8 +2589,14 @@ class VideoDetailView : ConstraintLayout { if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, - call = { handleSelectVideoTrack(it) }); - }.toList().toTypedArray()) + call = { handleSelectVideoTrack(it) }).apply { + videoSourceItems.add(this); + if(selectedLanguage != null) { + if(it.language != selectedLanguage) + this.visibility = View.GONE; + } + }; + }).toList()) else null, if(bestAudioSources.isNotEmpty()) SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio", diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt index d30a4795..60d28a5f 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt @@ -11,6 +11,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.dp class SlideUpMenuButtonList : LinearLayout { private val _root: LinearLayout; @@ -20,10 +21,16 @@ class SlideUpMenuButtonList : LinearLayout { var _activeText: String? = null; val id: String? - constructor(context: Context, attrs: AttributeSet? = null, id: String? = null): super(context, attrs) { - this.id = id + val scrollable: Boolean; - LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_button_list, this, true); + constructor(context: Context, attrs: AttributeSet? = null, id: String? = null, scrollable: Boolean = false): super(context, attrs) { + this.id = id + this.scrollable = scrollable ?: false; + + LayoutInflater.from(context).inflate( + if(!scrollable) + R.layout.overlay_slide_up_menu_button_list + else R.layout.overlay_slide_up_menu_button_list_scrollable, this, true); _root = findViewById(R.id.root); } @@ -37,8 +44,9 @@ class SlideUpMenuButtonList : LinearLayout { buttons.clear(); for (t in texts) { val button = LinearLayout(context); - button.layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT).apply { - weight = 1.0f; + button.layoutParams = LinearLayout.LayoutParams(if(!scrollable) 0 else LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT).apply { + if(!scrollable) + weight = 1.0f; marginStart = marginLeft; marginEnd = marginRight; }; @@ -49,7 +57,11 @@ class SlideUpMenuButtonList : LinearLayout { onClick.emit(t); }; - button.setPadding(0, 0, 0, 0); + val dp8 = 8.dp(resources) + if(!scrollable) + button.setPadding(0, 0, 0, 0); + else + button.setPadding(dp8, 0, dp8, 0); val text = TextView(context); text.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); @@ -69,6 +81,18 @@ class SlideUpMenuButtonList : LinearLayout { fun setSelected(text: String) { buttons[_activeText]?.background = ContextCompat.getDrawable(context, R.drawable.background_slide_up_option); buttons[text]?.background = ContextCompat.getDrawable(context, R.drawable.background_slide_up_option_selected); + + + val dp8 = 8.dp(resources) + if(!scrollable) { + buttons[text]?.setPadding(0, 0, 0, 0); + buttons[_activeText]?.setPadding(0, 0, 0, 0); + } + else { + buttons[text]?.setPadding(dp8, 0, dp8, 0); + buttons[_activeText]?.setPadding(dp8, 0, dp8, 0); + } + _activeText = text; } } \ No newline at end of file diff --git a/app/src/main/res/layout/overlay_slide_up_menu_button_list_scrollable.xml b/app/src/main/res/layout/overlay_slide_up_menu_button_list_scrollable.xml new file mode 100644 index 00000000..8b832105 --- /dev/null +++ b/app/src/main/res/layout/overlay_slide_up_menu_button_list_scrollable.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file From 8437825dd13f8cc1eb94fda6dcc717e6fa828da7 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 17 Dec 2025 20:29:45 +0100 Subject: [PATCH 4/4] apply language filters to downloads --- .../futo/platformplayer/UISlideOverlays.kt | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 75681154..2a1f1219 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -5,6 +5,7 @@ import android.content.ContentResolver import android.content.Context import android.content.Intent import android.net.Uri +import android.util.Log import android.view.View import android.view.ViewGroup import androidx.annotation.OptIn @@ -74,6 +75,8 @@ import kotlinx.coroutines.withContext import java.io.ByteArrayInputStream import androidx.core.net.toUri import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment +import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList +import kotlin.collections.toList class UISlideOverlays { companion object { @@ -573,6 +576,51 @@ class UISlideOverlays { return null; } + + val allLanguages = videoSources?.map { it.language }?.distinct() ?: listOf(); + val langResCombinations = if(videoSources != null) allLanguages.flatMap { + lang -> videoSources + .filter { v -> v.language == lang } + .map { it.height * it.width } + .distinct() + .map { res -> Pair(res, lang) } + } else listOf(); + var videoSourceItems = mutableListOf(); + var selectedLanguage: String? = null; + val languageFilters = if(allLanguages.filter { it != null }.count() > 1) + SlideUpMenuButtonList(container.context, null, "language_filter", true).apply { + var languageFilterLabels = allLanguages.filterNotNull().toList(); + val english = languageFilterLabels.find { it?.lowercase() == "en" }; + val originalLanguage = videoSources?.find { it.original == true }?.language; + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); + val hasPrimaryLanguage = videoSources?.any { it.language == primaryLanguage } ?: false; + + if(english != null) + languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList(); + if(primaryLanguage != null && languageFilterLabels.contains(primaryLanguage)) + languageFilterLabels = listOf(primaryLanguage).plus(languageFilterLabels.filter { it != primaryLanguage }).toList(); + if(originalLanguage != null) + languageFilterLabels = listOf(originalLanguage).plus(languageFilterLabels.filter { it != originalLanguage }).toList(); + Log.i(TAG, "Language filtesr: ${languageFilterLabels.joinToString(", ")}"); + selectedLanguage = originalLanguage ?: (if(hasPrimaryLanguage) primaryLanguage else null); + setButtons(languageFilterLabels, selectedLanguage); + onClick.subscribe { selected -> + setSelected(selected); + + videoSourceItems.forEach { + val item = it.itemTag; + if(item is IVideoSource) { + if(item.language == selected) + it.visibility = View.VISIBLE; + else + it.visibility = View.GONE; + } + } + } + } + else null; + + if(languageFilters != null) items.add(languageFilters) items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources, listOf((if (audioSources != null) listOf(SlideUpMenuItem( container.context, @@ -609,7 +657,13 @@ class UISlideOverlays { menu?.setOk(container.context.getString(R.string.download)); }, invokeParent = false - ) + ).apply { + videoSourceItems.add(this); + if(selectedLanguage != null) { + if(it.language != selectedLanguage) + this.visibility = View.GONE; + } + } } is JSDashManifestRawSource -> { @@ -629,7 +683,13 @@ class UISlideOverlays { menu?.setOk(container.context.getString(R.string.download)); }, invokeParent = false - ) + ).apply { + videoSourceItems.add(this); + if(selectedLanguage != null) { + if(it.language != selectedLanguage) + this.visibility = View.GONE; + } + } } is IHLSManifestSource -> { @@ -643,7 +703,13 @@ class UISlideOverlays { showHlsPicker(video, it, it.url, container) }, invokeParent = false - ) + ).apply { + videoSourceItems.add(this); + if(selectedLanguage != null) { + if(it.language != selectedLanguage) + this.visibility = View.GONE; + } + } } else -> {