From b04a1768702c4a0c558dfc252ba90d8e1eb405ae Mon Sep 17 00:00:00 2001 From: Sadwhy <99601717+Sadwhy@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:19:40 +0600 Subject: [PATCH] feat(exoplayer): custom subtitle view (#544) * branch * Add Custom Subtitles --- .../ani/dantotsu/media/anime/ExoplayerView.kt | 152 +++++++++++++++++- .../main/java/ani/dantotsu/others/Xubtitle.kt | 144 +++++++++++++++++ .../settings/PlayerSettingsActivity.kt | 81 +++++++--- .../dantotsu/settings/saving/Preferences.kt | 7 +- .../res/layout/activity_player_settings.xml | 128 ++++++++++++--- app/src/main/res/layout/exo_player_view.xml | 8 + app/src/main/res/values/strings.xml | 7 +- 7 files changed, 478 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/others/Xubtitle.kt diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index b8064851..e1961ca6 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -12,6 +12,7 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.res.Configuration +import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.Animatable import android.hardware.SensorManager @@ -71,9 +72,12 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player +import androidx.media3.common.text.Cue +import androidx.media3.common.text.CueGroup import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks +import androidx.media3.common.util.Util import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DataSource import androidx.media3.datasource.DefaultDataSource @@ -137,6 +141,7 @@ import ani.dantotsu.others.getSerialized import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.Subtitle +import ani.dantotsu.others.Xubtitle import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.VideoExtractor @@ -226,6 +231,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL private lateinit var animeTitle: TextView private lateinit var videoInfo: TextView private lateinit var episodeTitle: Spinner + private lateinit var customSubtitleView: Xubtitle private var orientationListener: OrientationEventListener? = null @@ -425,6 +431,95 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } } + private fun applySubtitleStyles(textView: Xubtitle) { + val primaryColor = when (PrefManager.getVal(PrefName.PrimaryColor)) { + 0 -> Color.BLACK + 1 -> Color.DKGRAY + 2 -> Color.GRAY + 3 -> Color.LTGRAY + 4 -> Color.WHITE + 5 -> Color.RED + 6 -> Color.YELLOW + 7 -> Color.GREEN + 8 -> Color.CYAN + 9 -> Color.BLUE + 10 -> Color.MAGENTA + 11 -> Color.TRANSPARENT + else -> Color.WHITE + } + + val subBackground = when (PrefManager.getVal(PrefName.SubBackground)) { + 0 -> Color.TRANSPARENT + 1 -> Color.BLACK + 2 -> Color.DKGRAY + 3 -> Color.GRAY + 4 -> Color.LTGRAY + 5 -> Color.WHITE + 6 -> Color.RED + 7 -> Color.YELLOW + 8 -> Color.GREEN + 9 -> Color.CYAN + 10 -> Color.BLUE + 11 -> Color.MAGENTA + else -> Color.TRANSPARENT + } + + val font = when (PrefManager.getVal(PrefName.Font)) { + 0 -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold) + 1 -> ResourcesCompat.getFont(this, R.font.poppins_bold) + 2 -> ResourcesCompat.getFont(this, R.font.poppins) + 3 -> ResourcesCompat.getFont(this, R.font.poppins_thin) + 4 -> ResourcesCompat.getFont(this, R.font.century_gothic_regular) + 5 -> ResourcesCompat.getFont(this, R.font.levenim_mt_bold) + 6 -> ResourcesCompat.getFont(this, R.font.blocky) + else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold) + } + + val fontSize = PrefManager.getVal(PrefName.FontSize).toFloat() + + textView.setBackgroundColor(subBackground) + textView.setTextColor(primaryColor) + textView.typeface = font + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize) + + val secondaryColor = when (PrefManager.getVal(PrefName.SecondaryColor)) { + 0 -> Color.BLACK + 1 -> Color.DKGRAY + 2 -> Color.GRAY + 3 -> Color.LTGRAY + 4 -> Color.WHITE + 5 -> Color.RED + 6 -> Color.YELLOW + 7 -> Color.GREEN + 8 -> Color.CYAN + 9 -> Color.BLUE + 10 -> Color.MAGENTA + 11 -> Color.TRANSPARENT + else -> Color.BLACK + } + + val subStroke = PrefManager.getVal(PrefName.SubStroke) + + textView.apply { + when (PrefManager.getVal(PrefName.Outline)) { + 0 -> applyOutline(secondaryColor, subStroke) + 1 -> applyShineEffect(secondaryColor) + 2 -> applyDropShadow(secondaryColor, subStroke) + 3 -> {} + else -> applyOutline(secondaryColor, subStroke) + } + } + + textView.alpha = + when (PrefManager.getVal(PrefName.Subtitles)) { + true -> PrefManager.getVal(PrefName.SubAlpha) + false -> 0f + } + + val textElevation = PrefManager.getVal(PrefName.SubBottomMargin) / 30 * resources.displayMetrics.heightPixels + textView.translationY = -textElevation + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -470,6 +565,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL skipTimeButton = playerView.findViewById(R.id.exo_skip_timestamp) skipTimeText = skipTimeButton.findViewById(R.id.exo_skip_timestamp_text) timeStampText = playerView.findViewById(R.id.exo_time_stamp_text) + customSubtitleView = playerView.findViewById(R.id.customSubtitleView) animeTitle = playerView.findViewById(R.id.exo_anime_title) episodeTitle = playerView.findViewById(R.id.exo_ep_sel) @@ -523,7 +619,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL it.visibility = View.GONE } } - setupSubFormatting(playerView) if (savedInstanceState != null) { currentWindow = savedInstanceState.getInt(resumeWindow) @@ -1732,6 +1827,54 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } playerView.player = exoPlayer + exoPlayer.addListener(object : Player.Listener { + var activeSubtitles = ArrayDeque(3) + var lastSubtitle: String? = null + var lastPosition: Long = 0 + + override fun onCues(cueGroup: CueGroup) { + if (PrefManager.getVal(PrefName.TextviewSubtitles)) { + exoSubtitleView.visibility = View.GONE + customSubtitleView.visibility = View.VISIBLE + val newCues = cueGroup.cues.map { it.text.toString() ?: "" } + + if (newCues.isEmpty()) { + customSubtitleView.text = "" + activeSubtitles.clear() + lastSubtitle = null + lastPosition = 0 + return + } + + val currentPosition = exoPlayer.currentPosition + + if ((lastSubtitle?.length ?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500)) { + activeSubtitles.clear() + } + + for (newCue in newCues) { + if (newCue !in activeSubtitles) { + if (activeSubtitles.size >= 2) { + activeSubtitles.removeLast() + } + activeSubtitles.addFirst(newCue) + lastSubtitle = newCue + lastPosition = currentPosition + } + } + + customSubtitleView.text = activeSubtitles.joinToString("\n") + } else { + customSubtitleView.text = "" + customSubtitleView.visibility = View.GONE + exoSubtitleView.visibility = View.VISIBLE + } + } + }) + + applySubtitleStyles(customSubtitleView) + setupSubFormatting(playerView) + try { val rightNow = Calendar.getInstance() mediaSession = MediaSession.Builder(this, exoPlayer) @@ -2021,7 +2164,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL TrackSelectionOverride(trackGroup.mediaTrackGroup, index) ) .build() - if (type == TRACK_TYPE_TEXT) setupSubFormatting(playerView) + if (type == TRACK_TYPE_TEXT) { + setupSubFormatting(playerView) + applySubtitleStyles(customSubtitleView) + } playerView.subtitleView?.alpha = when (isDisabled) { false -> PrefManager.getVal(PrefName.SubAlpha) true -> 0f @@ -2395,4 +2541,4 @@ class CustomCastButton : MediaRouteButton { true } } -} +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/others/Xubtitle.kt b/app/src/main/java/ani/dantotsu/others/Xubtitle.kt new file mode 100644 index 00000000..4fbf7bfd --- /dev/null +++ b/app/src/main/java/ani/dantotsu/others/Xubtitle.kt @@ -0,0 +1,144 @@ +package ani.dantotsu.others + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.LinearGradient +import android.graphics.Paint +import android.graphics.Shader +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView + +class Xubtitle + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + ) : AppCompatTextView(context, attrs, defStyleAttr) { + private var outlineThickness: Float = 0f + private var effectColor: Int = currentTextColor + private var currentEffect: Effect = Effect.NONE + + private val shadowPaint = Paint().apply { isAntiAlias = true } + private val outlinePaint = Paint().apply { isAntiAlias = true } + private var shineShader: Shader? = null + + enum class Effect { + NONE, + OUTLINE, + SHINE, + DROP_SHADOW, + } + + override fun onDraw(canvas: Canvas) { + val text = text.toString() + val textPaint = + TextPaint(paint).apply { + color = currentTextColor + } + val staticLayout = + StaticLayout.Builder + .obtain(text, 0, text.length, textPaint, width) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .setLineSpacing(0f, 1f) + .build() + + when (currentEffect) { + Effect.OUTLINE -> { + textPaint.style = Paint.Style.STROKE + textPaint.strokeWidth = outlineThickness + textPaint.color = effectColor + + staticLayout.draw(canvas) + + textPaint.style = Paint.Style.FILL + textPaint.color = currentTextColor + staticLayout.draw(canvas) + } + + Effect.DROP_SHADOW -> { + setLayerType(LAYER_TYPE_SOFTWARE, null) + textPaint.setShadowLayer(outlineThickness, 4f, 4f, effectColor) + + staticLayout.draw(canvas) + + textPaint.clearShadowLayer() + } + + Effect.SHINE -> { + val shadowShader = + LinearGradient( + 0f, + 0f, + width.toFloat(), + height.toFloat(), + intArrayOf(Color.WHITE, effectColor, Color.BLACK), + null, + Shader.TileMode.CLAMP, + ) + + val shadowPaint = + Paint().apply { + isAntiAlias = true + style = Paint.Style.FILL + textSize = textPaint.textSize + typeface = textPaint.typeface + shader = shadowShader + } + + canvas.drawText( + text, + x + 4f, // Shadow offset + y + 4f, + shadowPaint, + ) + + val shader = + LinearGradient( + 0f, + 0f, + width.toFloat(), + height.toFloat(), + intArrayOf(effectColor, Color.WHITE, Color.WHITE), + null, + Shader.TileMode.CLAMP, + ) + textPaint.shader = shader + staticLayout.draw(canvas) + textPaint.shader = null + } + + Effect.NONE -> { + staticLayout.draw(canvas) + } + } + } + + fun applyOutline( + color: Int, + outlineThickness: Float, + ) { + this.effectColor = color + this.outlineThickness = outlineThickness + currentEffect = Effect.OUTLINE + } + + // Too hard for me to figure it out + fun applyShineEffect(color: Int) { + this.effectColor = color + currentEffect = Effect.SHINE + } + + fun applyDropShadow( + color: Int, + outlineThickness: Float, + ) { + this.effectColor = color + this.outlineThickness = outlineThickness + currentEffect = Effect.DROP_SHADOW + } + } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt index 39b47cd2..c3192f99 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt @@ -267,21 +267,21 @@ class PlayerSettingsActivity : AppCompatActivity() { PrefManager.setVal(PrefName.Cast, isChecked) } + binding.playerSettingsRotate.isChecked = PrefManager.getVal(PrefName.RotationPlayer) + binding.playerSettingsRotate.setOnCheckedChangeListener { _, isChecked -> + PrefManager.setVal(PrefName.RotationPlayer, isChecked) + } + binding.playerSettingsInternalCast.isChecked = PrefManager.getVal(PrefName.UseInternalCast) binding.playerSettingsInternalCast.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.UseInternalCast, isChecked) } - binding.playerSettingsRotate.isChecked = PrefManager.getVal(PrefName.RotationPlayer) - binding.playerSettingsRotate.setOnCheckedChangeListener { _, isChecked -> - PrefManager.setVal(PrefName.RotationPlayer, isChecked) - } - binding.playerSettingsAdditionalCodec.isChecked = PrefManager.getVal(PrefName.UseAdditionalCodec) binding.playerSettingsAdditionalCodec.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.UseAdditionalCodec, isChecked) } - + val resizeModes = arrayOf("Original", "Zoom", "Stretch") binding.playerResizeMode.setOnClickListener { customAlertDialog().apply { @@ -306,23 +306,50 @@ class PlayerSettingsActivity : AppCompatActivity() { binding.videoSubColorWindow, binding.videoSubFont, binding.videoSubAlpha, + binding.videoSubStroke, binding.subtitleFontSizeText, - binding.subtitleFontSize + binding.subtitleFontSize, + binding.videoSubLanguage, + binding.subTextSwitch ).forEach { it.isEnabled = isChecked - it.isClickable = isChecked it.alpha = when (isChecked) { true -> 1f false -> 0.5f } } } + + fun toggleExpSubOptions(isChecked: Boolean) { + arrayOf( + binding.videoSubStrokeButton, + binding.videoSubStroke, + binding.videoSubBottomMarginButton, + binding.videoSubBottomMargin + ).forEach { + it.isEnabled = isChecked + it.alpha = when (isChecked) { + true -> 1f + false -> 0.5f + } + } + } + binding.subSwitch.isChecked = PrefManager.getVal(PrefName.Subtitles) binding.subSwitch.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.Subtitles, isChecked) toggleSubOptions(isChecked) + toggleExpSubOptions(binding.subTextSwitch.isChecked && isChecked) } toggleSubOptions(binding.subSwitch.isChecked) + + binding.subTextSwitch.isChecked = PrefManager.getVal(PrefName.TextviewSubtitles) + binding.subTextSwitch.setOnCheckedChangeListener { _, isChecked -> + PrefManager.setVal(PrefName.TextviewSubtitles, isChecked) + toggleExpSubOptions(isChecked) + } + toggleExpSubOptions(binding.subTextSwitch.isChecked) + val subLanguages = arrayOf( "Albanian", "Arabic", @@ -366,17 +393,17 @@ class PlayerSettingsActivity : AppCompatActivity() { "Urdu", "Vietnamese", ) - val subLanguageDialog = AlertDialog.Builder(this, R.style.MyPopup) - .setTitle(getString(R.string.subtitle_langauge)) binding.videoSubLanguage.setOnClickListener { - val dialog = subLanguageDialog.setSingleChoiceItems( - subLanguages, - PrefManager.getVal(PrefName.SubLanguage) - ) { dialog, count -> - PrefManager.setVal(PrefName.SubLanguage, count) - dialog.dismiss() - }.show() - dialog.window?.setDimAmount(0.8f) + customAlertDialog().apply { + setTitle(getString(R.string.subtitle_langauge)) + singleChoiceItems( + subLanguages, + PrefManager.getVal(PrefName.SubLanguage) + ) { count -> + PrefManager.setVal(PrefName.SubLanguage, count) + } + show() + } } val colorsPrimary = arrayOf( @@ -510,6 +537,22 @@ class PlayerSettingsActivity : AppCompatActivity() { } }) + binding.videoSubStroke.value = PrefManager.getVal(PrefName.SubStroke) + binding.videoSubStroke.addOnChangeListener(OnChangeListener { _, value, fromUser -> + if (fromUser) { + PrefManager.setVal(PrefName.SubStroke, value) + updateSubPreview() + } + }) + + binding.videoSubBottomMargin.value = PrefManager.getVal(PrefName.SubBottomMargin) + binding.videoSubBottomMargin.addOnChangeListener(OnChangeListener { _, value, fromUser -> + if (fromUser) { + PrefManager.setVal(PrefName.SubBottomMargin, value) + updateSubPreview() + } + }) + val fonts = arrayOf( "Poppins Semi Bold", "Poppins Bold", @@ -625,4 +668,4 @@ class PlayerSettingsActivity : AppCompatActivity() { ) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index af11c419..5aa9cdf4 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -94,6 +94,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files CursedSpeeds(Pref(Location.Player, Boolean::class, false)), Resize(Pref(Location.Player, Int::class, 0)), Subtitles(Pref(Location.Player, Boolean::class, true)), + TextviewSubtitles(Pref(Location.Player, Boolean::class, false)), SubLanguage(Pref(Location.Player, Int::class, 9)), PrimaryColor(Pref(Location.Player, Int::class, 4)), SecondaryColor(Pref(Location.Player, Int::class, 0)), @@ -101,6 +102,8 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files SubBackground(Pref(Location.Player, Int::class, 0)), SubWindow(Pref(Location.Player, Int::class, 0)), SubAlpha(Pref(Location.Player, Float::class, 1f)), + SubStroke(Pref(Location.Player, Float::class, 8f)), + SubBottomMargin(Pref(Location.Player, Float::class, 4f)), Font(Pref(Location.Player, Int::class, 0)), FontSize(Pref(Location.Player, Int::class, 20)), Locale(Pref(Location.Player, Int::class, 2)), @@ -128,7 +131,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files Pip(Pref(Location.Player, Boolean::class, true)), RotationPlayer(Pref(Location.Player, Boolean::class, true)), TorrentEnabled(Pref(Location.Player, Boolean::class, false)), - UseAdditionalCodec(Pref(Location.Player, Boolean::class, true)), + UseAdditionalCodec(Pref(Location.Player, Boolean::class, false)), //Reader ShowSource(Pref(Location.Reader, Boolean::class, true)), @@ -217,4 +220,4 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files Socks5ProxyPort(Pref(Location.Protected, String::class, "")), Socks5ProxyUsername(Pref(Location.Protected, String::class, "")), Socks5ProxyPassword(Pref(Location.Protected, String::class, "")), -} +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_player_settings.xml b/app/src/main/res/layout/activity_player_settings.xml index 2a608eee..d5fb0ebb 100644 --- a/app/src/main/res/layout/activity_player_settings.xml +++ b/app/src/main/res/layout/activity_player_settings.xml @@ -381,6 +381,88 @@ + + +