Compare commits

...

7 Commits

Author SHA1 Message Date
Koen J c84cea9ea1 Remove animation for quality selector. 2025-12-18 14:37:44 +01:00
Koen J 5c162083d5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-12-18 08:23:26 +01:00
Koen J 3230e7c0b4 Draft fix for cast subtitles UMP. 2025-12-18 08:23:13 +01:00
Kelvin 8437825dd1 apply language filters to downloads 2025-12-17 20:29:45 +01:00
Kelvin 0fbe0bb438 Add filters for video languages to resolve excessive sources 2025-12-17 19:43:56 +01:00
Kelvin 34d2e62314 sub mods 2025-12-17 16:27:12 +01:00
Kelvin 1075ded170 Language for video support, original for video support, deduplication fix for languages on videos, submods 2025-12-17 15:32:37 +01:00
23 changed files with 340 additions and 41 deletions
+7
View File
@@ -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;
}
}
@@ -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";
@@ -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<SlideUpMenuItem>();
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 -> {
@@ -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;
}
@@ -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;
}
@@ -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
}
@@ -9,4 +9,6 @@ interface IVideoSource {
val bitrate : Int?;
val duration: Long;
val priority: Boolean;
val language: String?;
val original: Boolean?;
}
@@ -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;
@@ -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;
}
@@ -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<String>(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<String?> {
val videoDashDef = video.generateAsync(scope);
val audioDashDef = audio.generateAsync(scope);
@@ -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 {
@@ -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? {
@@ -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<Int>(config, "duration", contextName).toLong();
priority = obj.getOrNull(config, "priority", contextName) ?: false;
language = _obj.getOrNull(config, "language", contextName);
original = _obj.getOrNull(config, "original", contextName);
}
}
@@ -44,6 +44,9 @@ open class JSVideoUrlSource(
override var priority: Boolean =
_obj.getOrDefault<Boolean>(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 =
@@ -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) {
@@ -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) {
@@ -1241,6 +1241,47 @@ abstract class StateCasting {
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
}
private fun escapeXml(s: String): String =
s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;")
private fun injectSubtitleAdaptationSet(
mpd: String,
subtitleUrl: String,
mimeType: String,
lang: String = "und",
label: String = "Subtitles"
): String {
val mt = mimeType.lowercase()
val codecs = when (mt) {
"text/vtt", "text/webvtt" -> "wvtt"
"application/ttml+xml", "application/ttml" -> "stpp"
else -> null
}
val codecsAttr = codecs?.let { " codecs=\"${escapeXml(it)}\"" } ?: ""
val adaptation = """
<AdaptationSet id="subtitles-1" contentType="text" mimeType="${escapeXml(mimeType)}" lang="${escapeXml(lang)}">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
<Label>${escapeXml(label)}</Label>
<Representation id="subtitles-1"$codecsAttr bandwidth="256" mimeType="${escapeXml(mimeType)}">
<BaseURL>${escapeXml(subtitleUrl)}</BaseURL>
</Representation>
</AdaptationSet>
""".trimIndent()
val periodClose = Regex("</Period\\s*>", RegexOption.IGNORE_CASE)
return if (periodClose.containsMatchIn(mpd)) {
mpd.replaceFirst(periodClose, adaptation + "\n</Period>")
} else {
mpd
}
}
@OptIn(UnstableApi::class)
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?, castId: Int, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null) : List<String> {
val ad = activeDevice ?: return listOf();
@@ -1262,30 +1303,42 @@ abstract class StateCasting {
val videoUrl = url + videoPath
val audioUrl = url + audioPath
val subtitleMimeTypeFull = subtitleSource?.format ?: "text/vtt"
val subtitleMimeTypeForMpd = subtitleMimeTypeFull.substringBefore(';').trim()
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
return@withContext subtitleSource.getSubtitlesURI();
} else null;
subtitleSource.getSubtitlesURI()
} else null
var subtitlesUrl: String? = null;
var subtitlesUrl: String? = null
if (subtitlesUri != null) {
if(subtitlesUri.scheme == "file") {
var content: String? = null;
val inputStream = contentResolver.openInputStream(subtitlesUri);
inputStream?.use { stream ->
val reader = stream.bufferedReader();
content = reader.use { it.readText() };
when (subtitlesUri.scheme) {
"file", "content" -> {
val content = withContext(Dispatchers.IO) {
contentResolver.openInputStream(subtitlesUri)?.use { stream ->
stream.bufferedReader().use { it.readText() }
}
}
if (!content.isNullOrEmpty()) {
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", subtitlePath, content, subtitleMimeTypeFull)
.withHeader("Access-Control-Allow-Origin", "*"),
true
).withTag("castDashRaw")
subtitlesUrl = url + subtitlePath
}
}
if (content != null) {
_castServer.addHandlerWithAllowAllOptions(
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
.withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast");
"http", "https" -> {
// Receiver will fetch directly (works only if it doesnt need auth/headers)
subtitlesUrl = subtitlesUri.toString()
}
subtitlesUrl = url + subtitlePath;
} else {
subtitlesUrl = subtitlesUri.toString();
else -> {
Logger.w(TAG, "Unsupported subtitlesUri scheme: ${subtitlesUri.scheme}")
}
}
}
@@ -1331,6 +1384,14 @@ abstract class StateCasting {
return emptyList()
}
if (subtitlesUrl != null) {
dashContent = injectSubtitleAdaptationSet(
dashContent,
subtitlesUrl!!,
subtitleMimeTypeForMpd
)
}
var hasAudioInDash = false
for (representation in representationRegex.findAll(dashContent)) {
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
@@ -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,9 +2424,54 @@ 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 }?.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();
Log.i(TAG, "Language count: ${allLanguages}");
var videoSourceItems = mutableListOf<SlideUpMenuItem>();
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 }))
?.distinct()
?.filterNotNull()
@@ -2441,7 +2487,7 @@ class VideoDetailView : ConstraintLayout {
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
R.string.quality), null, true,
R.string.quality), null, false,
qualityPlaybackSpeedTitle,
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
@@ -2531,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,
@@ -2544,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",
@@ -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<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>, preferredLanguage: String? = null) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers, preferredLanguage);
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>, 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 };
@@ -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;
}
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:id="@+id/root"
android:orientation="horizontal"
android:paddingStart="0dp"
android:paddingEnd="0dp">
</LinearLayout>
</HorizontalScrollView>