From dd268cb10ee2e89d0fa2a226d4404d8fd996da47 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sat, 24 Jun 2023 18:57:33 +0200 Subject: [PATCH] Chapters dialog: highlight current chapter and auto scroll to it --- .../github/libretube/helpers/PlayerHelper.kt | 52 +++++++++++++++---- .../libretube/ui/fragments/PlayerFragment.kt | 27 +++------- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt index d40ab8ce7..66c27c9d1 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -6,6 +6,8 @@ import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.net.Uri +import android.os.Handler +import android.os.Looper import android.text.format.DateUtils import android.util.Base64 import android.view.accessibility.CaptioningManager @@ -16,6 +18,7 @@ import androidx.core.app.RemoteActionCompat import androidx.core.content.getSystemService import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri +import androidx.core.view.children import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.PlaybackParameters @@ -46,7 +49,8 @@ object PlayerHelper { "outro", "filler", "music_offtopic", - "preview") + "preview" + ) /** * Create a base64 encoded DASH stream manifest @@ -84,9 +88,9 @@ object PlayerHelper { fun getSponsorBlockCategories(): MutableMap { val categories: MutableMap = mutableMapOf() - for (category in SPONSOR_CATEGORIES){ + for (category in SPONSOR_CATEGORIES) { val state = PreferenceHelper.getString(category + "_category", "off").uppercase() - if (SbSkipOptions.valueOf(state) != SbSkipOptions.OFF){ + if (SbSkipOptions.valueOf(state) != SbSkipOptions.OFF) { categories[category] = SbSkipOptions.valueOf(state) } } @@ -109,6 +113,7 @@ object PlayerHelper { ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } } + "auto" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR "landscape" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE "portrait" -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT @@ -231,7 +236,9 @@ object PlayerHelper { private val backgroundSpeed: Float get() = when (PreferenceHelper.getBoolean(PreferenceKeys.CUSTOM_PLAYBACK_SPEED, false)) { - true -> PreferenceHelper.getString(PreferenceKeys.BACKGROUND_PLAYBACK_SPEED, "1").toFloat() + true -> PreferenceHelper.getString(PreferenceKeys.BACKGROUND_PLAYBACK_SPEED, "1") + .toFloat() + else -> playbackSpeed } @@ -427,8 +434,7 @@ object PlayerHelper { * Check for SponsorBlock segments matching the current player position * @param context A main dispatcher context * @param segments List of the SponsorBlock segments - * @param skipManually Whether the event gets handled by the function caller - * @return If segment found and [skipManually] is true, the end position of the segment in ms, otherwise null + * @return If segment found and should skip manually, the end position of the segment in ms, otherwise null */ fun ExoPlayer.checkForSegments( context: Context, @@ -465,11 +471,19 @@ object PlayerHelper { for (segment in segments) { val segmentStart = (segment.segment[0] * 1000f).toLong() val segmentEnd = (segment.segment[1] * 1000f).toLong() - if (currentPosition in segmentStart..segmentEnd) { return true } + if (currentPosition in segmentStart..segmentEnd) return true } return false } + /** + * Get the name of the currently played chapter + */ + fun getCurrentChapterIndex(exoPlayer: ExoPlayer, chapters: List): Int? { + val currentPosition = exoPlayer.currentPosition / 1000 + return chapters.indexOfLast { currentPosition >= it.start }.takeIf { it >= 0 } + } + /** * Show a dialog with the chapters provided, even if the list is empty */ @@ -477,11 +491,31 @@ object PlayerHelper { val titles = chapters.map { chapter -> "(${DateUtils.formatElapsedTime(chapter.start)}) ${chapter.title}" } - MaterialAlertDialogBuilder(context) + val dialog = MaterialAlertDialogBuilder(context) .setTitle(R.string.chapters) .setItems(titles.toTypedArray()) { _, index -> player.seekTo(chapters[index].start * 1000) } - .show() + .create() + val handler = Handler(Looper.getMainLooper()) + + val updatePosition = Runnable { + // scroll to the current playing index in the chapter + val currentPosition = + getCurrentChapterIndex(player, chapters) ?: return@Runnable + dialog.listView.smoothScrollToPosition(currentPosition) + val current = dialog.listView.children.toList()[currentPosition] + val highlightColor = + ThemeHelper.getThemeColor(context, android.R.attr.colorControlHighlight) + current.setBackgroundColor(highlightColor) + } + + dialog.setOnShowListener { + updatePosition.run() + // update the position after a short delay + if (dialog.isShowing) handler.postDelayed(updatePosition, 200) + } + + dialog.show() } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 5a556c730..50a92458d 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -582,12 +582,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { binding.playerViewsInfo.text = viewInfo if (this::chapters.isInitialized && chapters.isNotEmpty()) { - val chapterIndex = getCurrentChapterIndex() ?: return - // scroll to the current chapter in the chapterRecView in the description - binding.chaptersRecView.scrollToPosition(chapterIndex) - // set selected item, that should be highlighted - val chaptersAdapter = binding.chaptersRecView.adapter as ChaptersAdapter - chaptersAdapter.updateSelectedPosition(chapterIndex) + setCurrentChapterName(true, false) } } @@ -1198,17 +1193,17 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } // set the name of the video chapter in the exoPlayerView - private fun setCurrentChapterName(position: Long? = null) { + private fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) { // return if chapters are empty to avoid crashes if (chapters.isEmpty() || _binding == null) return // call the function again in 100ms - binding.player.postDelayed(this::setCurrentChapterName, 100) + if (enqueueNew) binding.player.postDelayed(this::setCurrentChapterName, 100) // if the user is scrubbing the time bar, don't update - if (scrubbingTimeBar && position == null) return + if (scrubbingTimeBar && !forceUpdate) return - val chapterIndex = getCurrentChapterIndex(position) ?: return + val chapterIndex = PlayerHelper.getCurrentChapterIndex(exoPlayer, chapters) ?: return val chapterName = chapters[chapterIndex].title.trim() // change the chapter name textView text to the chapterName @@ -1220,14 +1215,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } } - /** - * Get the name of the currently played chapter - */ - private fun getCurrentChapterIndex(position: Long? = null): Int? { - val currentPosition = (position ?: exoPlayer.currentPosition) / 1000 - return chapters.indexOfLast { currentPosition >= it.start }.takeIf { it >= 0 } - } - private fun setMediaSource(uri: Uri, mimeType: String) { val mediaItem = MediaItem.Builder() .setUri(uri) @@ -1533,12 +1520,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { playerBinding, streams.duration * 1000, onScrub = { - setCurrentChapterName(it) + setCurrentChapterName(forceUpdate = true, enqueueNew = false) scrubbingTimeBar = true }, onScrubEnd = { scrubbingTimeBar = false - setCurrentChapterName(it) + setCurrentChapterName(forceUpdate = true, enqueueNew = false) }, ), )