mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 16:30:31 +05:30
Merge pull request #6055 from Bnyro/master
fix: chapters dont work in audio mode
This commit is contained in:
commit
5e1e19fb8e
@ -45,6 +45,7 @@ import com.github.libretube.obj.PlayerNotificationData
|
|||||||
import com.github.libretube.ui.base.BaseActivity
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
import com.github.libretube.ui.interfaces.TimeFrameReceiver
|
import com.github.libretube.ui.interfaces.TimeFrameReceiver
|
||||||
import com.github.libretube.ui.listeners.SeekbarPreviewListener
|
import com.github.libretube.ui.listeners.SeekbarPreviewListener
|
||||||
|
import com.github.libretube.ui.models.ChaptersViewModel
|
||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
import com.github.libretube.util.NowPlayingNotification
|
import com.github.libretube.util.NowPlayingNotification
|
||||||
import com.github.libretube.util.OfflineTimeFrameReceiver
|
import com.github.libretube.util.OfflineTimeFrameReceiver
|
||||||
@ -66,6 +67,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
|
|
||||||
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
|
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
|
||||||
private val playerViewModel: PlayerViewModel by viewModels()
|
private val playerViewModel: PlayerViewModel by viewModels()
|
||||||
|
private val chaptersViewModel: ChaptersViewModel by viewModels()
|
||||||
|
|
||||||
private val watchPositionTimer = PauseableTimer(
|
private val watchPositionTimer = PauseableTimer(
|
||||||
onTick = this::saveWatchPosition,
|
onTick = this::saveWatchPosition,
|
||||||
@ -177,7 +179,8 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
|
|
||||||
binding.player.initialize(
|
binding.player.initialize(
|
||||||
binding.doubleTapOverlay.binding,
|
binding.doubleTapOverlay.binding,
|
||||||
binding.playerGestureControlsView.binding
|
binding.playerGestureControlsView.binding,
|
||||||
|
chaptersViewModel
|
||||||
)
|
)
|
||||||
|
|
||||||
nowPlayingNotification = NowPlayingNotification(this, player, NowPlayingNotification.Companion.NowPlayingNotificationType.VIDEO_OFFLINE)
|
nowPlayingNotification = NowPlayingNotification(this, player, NowPlayingNotification.Companion.NowPlayingNotificationType.VIDEO_OFFLINE)
|
||||||
@ -189,7 +192,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
|||||||
Database.downloadDao().findById(videoId)
|
Database.downloadDao().findById(videoId)
|
||||||
}
|
}
|
||||||
val chapters = downloadChapters.map(DownloadChapter::toChapterSegment)
|
val chapters = downloadChapters.map(DownloadChapter::toChapterSegment)
|
||||||
playerViewModel.chaptersLiveData.value = chapters
|
chaptersViewModel.chaptersLiveData.value = chapters
|
||||||
binding.player.setChapters(chapters)
|
binding.player.setChapters(chapters)
|
||||||
|
|
||||||
val downloadFiles = downloadItems.filter { it.path.exists() }
|
val downloadFiles = downloadItems.filter { it.path.exists() }
|
||||||
|
@ -14,7 +14,7 @@ import com.github.libretube.ui.viewholders.ChaptersViewHolder
|
|||||||
|
|
||||||
class ChaptersAdapter(
|
class ChaptersAdapter(
|
||||||
var chapters: List<ChapterSegment>,
|
var chapters: List<ChapterSegment>,
|
||||||
private val videoDuration: Long,
|
private val videoDurationSeconds: Long,
|
||||||
private val seekTo: (Long) -> Unit
|
private val seekTo: (Long) -> Unit
|
||||||
) : RecyclerView.Adapter<ChaptersViewHolder>() {
|
) : RecyclerView.Adapter<ChaptersViewHolder>() {
|
||||||
private var selectedPosition = 0
|
private var selectedPosition = 0
|
||||||
@ -36,12 +36,11 @@ class ChaptersAdapter(
|
|||||||
chapterTitle.text = chapter.title
|
chapterTitle.text = chapter.title
|
||||||
timeStamp.text = DateUtils.formatElapsedTime(chapter.start)
|
timeStamp.text = DateUtils.formatElapsedTime(chapter.start)
|
||||||
|
|
||||||
val playerDurationSeconds = videoDuration / 1000
|
|
||||||
val chapterEnd = if (chapter.highlightDrawable == null) {
|
val chapterEnd = if (chapter.highlightDrawable == null) {
|
||||||
chapters.getOrNull(position + 1)?.start ?: playerDurationSeconds
|
chapters.getOrNull(position + 1)?.start ?: videoDurationSeconds
|
||||||
} else {
|
} else {
|
||||||
// the duration for chapters is hardcoded, since it's not provided by the SB API
|
// the duration for chapters is hardcoded, since it's not provided by the SB API
|
||||||
minOf(chapter.start + ChapterSegment.HIGHLIGHT_LENGTH, playerDurationSeconds)
|
minOf(chapter.start + ChapterSegment.HIGHLIGHT_LENGTH, videoDurationSeconds)
|
||||||
}
|
}
|
||||||
val durationSpan = chapterEnd - chapter.start
|
val durationSpan = chapterEnd - chapter.start
|
||||||
duration.text = root.context.getString(
|
duration.text = root.context.getString(
|
||||||
|
@ -31,6 +31,7 @@ import com.github.libretube.extensions.normalize
|
|||||||
import com.github.libretube.extensions.seekBy
|
import com.github.libretube.extensions.seekBy
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
import com.github.libretube.extensions.togglePlayPauseState
|
import com.github.libretube.extensions.togglePlayPauseState
|
||||||
|
import com.github.libretube.extensions.updateIfChanged
|
||||||
import com.github.libretube.helpers.AudioHelper
|
import com.github.libretube.helpers.AudioHelper
|
||||||
import com.github.libretube.helpers.BackgroundHelper
|
import com.github.libretube.helpers.BackgroundHelper
|
||||||
import com.github.libretube.helpers.ImageHelper
|
import com.github.libretube.helpers.ImageHelper
|
||||||
@ -42,6 +43,7 @@ import com.github.libretube.services.OnlinePlayerService
|
|||||||
import com.github.libretube.ui.activities.MainActivity
|
import com.github.libretube.ui.activities.MainActivity
|
||||||
import com.github.libretube.ui.interfaces.AudioPlayerOptions
|
import com.github.libretube.ui.interfaces.AudioPlayerOptions
|
||||||
import com.github.libretube.ui.listeners.AudioPlayerThumbnailListener
|
import com.github.libretube.ui.listeners.AudioPlayerThumbnailListener
|
||||||
|
import com.github.libretube.ui.models.ChaptersViewModel
|
||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
import com.github.libretube.ui.sheets.ChaptersBottomSheet
|
import com.github.libretube.ui.sheets.ChaptersBottomSheet
|
||||||
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
|
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
|
||||||
@ -61,6 +63,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
private lateinit var audioHelper: AudioHelper
|
private lateinit var audioHelper: AudioHelper
|
||||||
private val mainActivity get() = context as MainActivity
|
private val mainActivity get() = context as MainActivity
|
||||||
private val viewModel: PlayerViewModel by activityViewModels()
|
private val viewModel: PlayerViewModel by activityViewModels()
|
||||||
|
private val chaptersModel: ChaptersViewModel by activityViewModels()
|
||||||
|
|
||||||
// for the transition
|
// for the transition
|
||||||
private var transitionStartId = 0
|
private var transitionStartId = 0
|
||||||
@ -167,12 +170,24 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requireActivity().supportFragmentManager.setFragmentResultListener(
|
||||||
|
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
|
||||||
|
viewLifecycleOwner
|
||||||
|
) { _, bundle ->
|
||||||
|
playerService?.player?.seekTo(bundle.getLong(IntentData.currentPosition))
|
||||||
|
}
|
||||||
|
|
||||||
binding.openChapters.setOnClickListener {
|
binding.openChapters.setOnClickListener {
|
||||||
val playerService = playerService ?: return@setOnClickListener
|
val playerService = playerService ?: return@setOnClickListener
|
||||||
viewModel.chaptersLiveData.value = playerService.streams?.chapters.orEmpty()
|
chaptersModel.chaptersLiveData.value = playerService.streams?.chapters.orEmpty()
|
||||||
|
|
||||||
ChaptersBottomSheet()
|
ChaptersBottomSheet()
|
||||||
.show(childFragmentManager)
|
.apply {
|
||||||
|
arguments = bundleOf(
|
||||||
|
IntentData.duration to playerService.player?.duration?.div(1000)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.show(requireActivity().supportFragmentManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.miniPlayerClose.setOnClickListener {
|
binding.miniPlayerClose.setOnClickListener {
|
||||||
@ -205,6 +220,8 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!PlayerHelper.playAutomatically) updatePlayPauseButton()
|
if (!PlayerHelper.playAutomatically) updatePlayPauseButton()
|
||||||
|
|
||||||
|
updateChapterIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun killFragment() {
|
private fun killFragment() {
|
||||||
@ -435,4 +452,14 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
|
|||||||
|
|
||||||
binding.volumeTextView.text = "${bar.progress.normalize(0, bar.max, 0, 100)}"
|
binding.volumeTextView.text = "${bar.progress.normalize(0, bar.max, 0, 100)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateChapterIndex() {
|
||||||
|
if (_binding == null) return
|
||||||
|
handler.postDelayed(this::updateChapterIndex, 100)
|
||||||
|
|
||||||
|
val player = playerService?.player ?: return
|
||||||
|
|
||||||
|
val currentIndex = PlayerHelper.getCurrentChapterIndex(player.currentPosition, chaptersModel.chapters)
|
||||||
|
chaptersModel.currentChapterIndex.updateIfChanged(currentIndex ?: return)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,7 @@ import com.github.libretube.ui.extensions.animateDown
|
|||||||
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||||
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
|
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
|
||||||
import com.github.libretube.ui.listeners.SeekbarPreviewListener
|
import com.github.libretube.ui.listeners.SeekbarPreviewListener
|
||||||
|
import com.github.libretube.ui.models.ChaptersViewModel
|
||||||
import com.github.libretube.ui.models.CommentsViewModel
|
import com.github.libretube.ui.models.CommentsViewModel
|
||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||||
@ -127,6 +128,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
|
|
||||||
private val viewModel: PlayerViewModel by activityViewModels()
|
private val viewModel: PlayerViewModel by activityViewModels()
|
||||||
private val commentsViewModel: CommentsViewModel by activityViewModels()
|
private val commentsViewModel: CommentsViewModel by activityViewModels()
|
||||||
|
private val chaptersViewModel: ChaptersViewModel by activityViewModels()
|
||||||
|
|
||||||
// Video information passed by the intent
|
// Video information passed by the intent
|
||||||
private lateinit var videoId: String
|
private lateinit var videoId: String
|
||||||
@ -467,6 +469,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
(activity as MainActivity).requestOrientationChange()
|
(activity as MainActivity).requestOrientationChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMaxSheetHeight()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -634,7 +638,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMaxSheetHeight() {
|
private fun updateMaxSheetHeight() {
|
||||||
viewModel.maxSheetHeightPx = binding.root.height - binding.player.height
|
val maxHeight = binding.root.height - binding.player.height
|
||||||
|
viewModel.maxSheetHeightPx = viewModel.maxSheetHeightPx
|
||||||
|
chaptersViewModel.maxSheetHeightPx = maxHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playOnBackground() {
|
private fun playOnBackground() {
|
||||||
@ -1022,7 +1028,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
private fun initializePlayerView() {
|
private fun initializePlayerView() {
|
||||||
// initialize the player view actions
|
// initialize the player view actions
|
||||||
binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding)
|
binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding, chaptersViewModel)
|
||||||
binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this)
|
binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this)
|
||||||
|
|
||||||
binding.descriptionLayout.setStreams(streams)
|
binding.descriptionLayout.setStreams(streams)
|
||||||
@ -1040,7 +1046,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
playerBinding.exoTitle.text = streams.title
|
playerBinding.exoTitle.text = streams.title
|
||||||
|
|
||||||
// init the chapters recyclerview
|
// init the chapters recyclerview
|
||||||
viewModel.chaptersLiveData.value = streams.chapters
|
chaptersViewModel.chaptersLiveData.value = streams.chapters
|
||||||
|
|
||||||
if (PlayerHelper.relatedStreamsEnabled) {
|
if (PlayerHelper.relatedStreamsEnabled) {
|
||||||
val relatedLayoutManager = binding.relatedRecView.layoutManager as LinearLayoutManager
|
val relatedLayoutManager = binding.relatedRecView.layoutManager as LinearLayoutManager
|
||||||
@ -1132,8 +1138,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
start = highlightStart,
|
start = highlightStart,
|
||||||
highlightDrawable = frame?.toDrawable(requireContext().resources)
|
highlightDrawable = frame?.toDrawable(requireContext().resources)
|
||||||
)
|
)
|
||||||
viewModel.chaptersLiveData.postValue(
|
chaptersViewModel.chaptersLiveData.postValue(
|
||||||
viewModel.chapters.plus(highlightChapter).sortedBy { it.start }
|
chaptersViewModel.chapters.plus(highlightChapter).sortedBy { it.start }
|
||||||
)
|
)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.github.libretube.ui.models
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.github.libretube.api.obj.ChapterSegment
|
||||||
|
|
||||||
|
class ChaptersViewModel: ViewModel() {
|
||||||
|
val chaptersLiveData = MutableLiveData<List<ChapterSegment>>()
|
||||||
|
val chapters get() = chaptersLiveData.value.orEmpty()
|
||||||
|
val currentChapterIndex = MutableLiveData<Int>()
|
||||||
|
var maxSheetHeightPx = 0
|
||||||
|
}
|
@ -10,7 +10,6 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
|||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.JsonHelper
|
import com.github.libretube.api.JsonHelper
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.obj.ChapterSegment
|
|
||||||
import com.github.libretube.api.obj.Message
|
import com.github.libretube.api.obj.Message
|
||||||
import com.github.libretube.api.obj.Segment
|
import com.github.libretube.api.obj.Segment
|
||||||
import com.github.libretube.api.obj.Streams
|
import com.github.libretube.api.obj.Streams
|
||||||
@ -48,9 +47,6 @@ class PlayerViewModel : ViewModel() {
|
|||||||
|
|
||||||
var maxSheetHeightPx = 0
|
var maxSheetHeightPx = 0
|
||||||
|
|
||||||
val chaptersLiveData = MutableLiveData<List<ChapterSegment>>()
|
|
||||||
|
|
||||||
val chapters get() = chaptersLiveData.value.orEmpty()
|
|
||||||
var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled
|
var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,29 +2,32 @@ package com.github.libretube.ui.sheets
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.fragment.app.setFragmentResult
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.databinding.BottomSheetBinding
|
import com.github.libretube.databinding.BottomSheetBinding
|
||||||
import com.github.libretube.helpers.PlayerHelper
|
|
||||||
import com.github.libretube.ui.adapters.ChaptersAdapter
|
import com.github.libretube.ui.adapters.ChaptersAdapter
|
||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.ChaptersViewModel
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
class ChaptersBottomSheet : UndimmedBottomSheet() {
|
class ChaptersBottomSheet : UndimmedBottomSheet() {
|
||||||
private var _binding: BottomSheetBinding? = null
|
private var _binding: BottomSheetBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
|
||||||
|
|
||||||
private val playerViewModel: PlayerViewModel by activityViewModels()
|
private val chaptersViewModel: ChaptersViewModel by activityViewModels()
|
||||||
|
private var duration = 0L
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
duration = requireArguments().getLong(IntentData.duration, 0L)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -35,61 +38,45 @@ class ChaptersBottomSheet : UndimmedBottomSheet() {
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private val updatePosition = object : Runnable {
|
|
||||||
override fun run() {
|
|
||||||
val binding = _binding ?: return
|
|
||||||
handler.postDelayed(this, 200)
|
|
||||||
|
|
||||||
val currentIndex = getCurrentIndex() ?: return
|
|
||||||
|
|
||||||
val adapter = binding.optionsRecycler.adapter as ChaptersAdapter
|
|
||||||
adapter.updateSelectedPosition(currentIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
binding.optionsRecycler.layoutManager = LinearLayoutManager(context)
|
binding.optionsRecycler.layoutManager = LinearLayoutManager(context)
|
||||||
val adapter =
|
val adapter =
|
||||||
ChaptersAdapter(playerViewModel.chapters, playerViewModel.player?.duration ?: 0) {
|
ChaptersAdapter(chaptersViewModel.chapters, duration) {
|
||||||
playerViewModel.player?.seekTo(it)
|
setFragmentResult(SEEK_TO_POSITION_REQUEST_KEY, bundleOf(IntentData.currentPosition to it))
|
||||||
}
|
}
|
||||||
binding.optionsRecycler.adapter = adapter
|
binding.optionsRecycler.adapter = adapter
|
||||||
|
|
||||||
binding.optionsRecycler.viewTreeObserver.addOnGlobalLayoutListener(
|
binding.optionsRecycler.viewTreeObserver.addOnGlobalLayoutListener(
|
||||||
object : ViewTreeObserver.OnGlobalLayoutListener {
|
object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
override fun onGlobalLayout() {
|
override fun onGlobalLayout() {
|
||||||
val currentIndex = getCurrentIndex() ?: return
|
chaptersViewModel.currentChapterIndex.value?.let {
|
||||||
binding.optionsRecycler.scrollToPosition(currentIndex)
|
binding.optionsRecycler.scrollToPosition(it)
|
||||||
|
}
|
||||||
|
|
||||||
binding.optionsRecycler.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
binding.optionsRecycler.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
chaptersViewModel.currentChapterIndex.observe(viewLifecycleOwner) { currentIndex ->
|
||||||
|
if (_binding == null) return@observe
|
||||||
|
|
||||||
|
adapter.updateSelectedPosition(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
binding.bottomSheetTitle.text = context?.getString(R.string.chapters)
|
binding.bottomSheetTitle.text = context?.getString(R.string.chapters)
|
||||||
binding.bottomSheetTitleLayout.isVisible = true
|
binding.bottomSheetTitleLayout.isVisible = true
|
||||||
|
|
||||||
playerViewModel.chaptersLiveData.observe(viewLifecycleOwner) {
|
chaptersViewModel.chaptersLiveData.observe(viewLifecycleOwner) {
|
||||||
adapter.chapters = it
|
adapter.chapters = it
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePosition.run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCurrentIndex(): Int? {
|
override fun getSheetMaxHeightPx() = chaptersViewModel.maxSheetHeightPx
|
||||||
val player = playerViewModel.player ?: return null
|
|
||||||
|
|
||||||
return PlayerHelper.getCurrentChapterIndex(
|
|
||||||
player.currentPosition,
|
|
||||||
playerViewModel.chapters
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSheetMaxHeightPx() = playerViewModel.maxSheetHeightPx
|
|
||||||
|
|
||||||
override fun getDragHandle() = binding.dragHandle
|
override fun getDragHandle() = binding.dragHandle
|
||||||
|
|
||||||
@ -99,4 +86,8 @@ class ChaptersBottomSheet : UndimmedBottomSheet() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SEEK_TO_POSITION_REQUEST_KEY = "seek_to_position_request_key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.TooltipCompat
|
import androidx.appcompat.widget.TooltipCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
@ -26,6 +27,7 @@ import androidx.core.view.isInvisible
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginStart
|
import androidx.core.view.marginStart
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.text.Cue
|
import androidx.media3.common.text.Cue
|
||||||
@ -36,7 +38,7 @@ import androidx.media3.ui.PlayerView
|
|||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.SubtitleView
|
||||||
import androidx.media3.ui.TimeBar
|
import androidx.media3.ui.TimeBar
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.obj.ChapterSegment
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.databinding.DoubleTapOverlayBinding
|
import com.github.libretube.databinding.DoubleTapOverlayBinding
|
||||||
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
||||||
@ -46,6 +48,7 @@ import com.github.libretube.extensions.normalize
|
|||||||
import com.github.libretube.extensions.round
|
import com.github.libretube.extensions.round
|
||||||
import com.github.libretube.extensions.seekBy
|
import com.github.libretube.extensions.seekBy
|
||||||
import com.github.libretube.extensions.togglePlayPauseState
|
import com.github.libretube.extensions.togglePlayPauseState
|
||||||
|
import com.github.libretube.extensions.updateIfChanged
|
||||||
import com.github.libretube.helpers.AudioHelper
|
import com.github.libretube.helpers.AudioHelper
|
||||||
import com.github.libretube.helpers.BrightnessHelper
|
import com.github.libretube.helpers.BrightnessHelper
|
||||||
import com.github.libretube.helpers.ContextHelper
|
import com.github.libretube.helpers.ContextHelper
|
||||||
@ -59,6 +62,7 @@ import com.github.libretube.ui.fragments.PlayerFragment
|
|||||||
import com.github.libretube.ui.interfaces.PlayerGestureOptions
|
import com.github.libretube.ui.interfaces.PlayerGestureOptions
|
||||||
import com.github.libretube.ui.interfaces.PlayerOptions
|
import com.github.libretube.ui.interfaces.PlayerOptions
|
||||||
import com.github.libretube.ui.listeners.PlayerGestureController
|
import com.github.libretube.ui.listeners.PlayerGestureController
|
||||||
|
import com.github.libretube.ui.models.ChaptersViewModel
|
||||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||||
import com.github.libretube.ui.sheets.ChaptersBottomSheet
|
import com.github.libretube.ui.sheets.ChaptersBottomSheet
|
||||||
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
|
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
|
||||||
@ -81,6 +85,7 @@ abstract class CustomExoPlayerView(
|
|||||||
private lateinit var playerGestureController: PlayerGestureController
|
private lateinit var playerGestureController: PlayerGestureController
|
||||||
private lateinit var brightnessHelper: BrightnessHelper
|
private lateinit var brightnessHelper: BrightnessHelper
|
||||||
private lateinit var audioHelper: AudioHelper
|
private lateinit var audioHelper: AudioHelper
|
||||||
|
private lateinit var chaptersViewModel: ChaptersViewModel
|
||||||
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
|
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
|
||||||
private var chaptersBottomSheet: ChaptersBottomSheet? = null
|
private var chaptersBottomSheet: ChaptersBottomSheet? = null
|
||||||
private var scrubbingTimeBar = false
|
private var scrubbingTimeBar = false
|
||||||
@ -114,10 +119,12 @@ abstract class CustomExoPlayerView(
|
|||||||
|
|
||||||
fun initialize(
|
fun initialize(
|
||||||
doubleTapOverlayBinding: DoubleTapOverlayBinding,
|
doubleTapOverlayBinding: DoubleTapOverlayBinding,
|
||||||
playerGestureControlsViewBinding: PlayerGestureControlsViewBinding
|
playerGestureControlsViewBinding: PlayerGestureControlsViewBinding,
|
||||||
|
chaptersViewModel: ChaptersViewModel
|
||||||
) {
|
) {
|
||||||
this.doubleTapOverlayBinding = doubleTapOverlayBinding
|
this.doubleTapOverlayBinding = doubleTapOverlayBinding
|
||||||
this.gestureViewBinding = playerGestureControlsViewBinding
|
this.gestureViewBinding = playerGestureControlsViewBinding
|
||||||
|
this.chaptersViewModel = chaptersViewModel
|
||||||
this.playerGestureController = PlayerGestureController(context as BaseActivity, this)
|
this.playerGestureController = PlayerGestureController(context as BaseActivity, this)
|
||||||
this.brightnessHelper = BrightnessHelper(context as Activity)
|
this.brightnessHelper = BrightnessHelper(context as Activity)
|
||||||
this.audioHelper = AudioHelper(context)
|
this.audioHelper = AudioHelper(context)
|
||||||
@ -216,11 +223,24 @@ abstract class CustomExoPlayerView(
|
|||||||
|
|
||||||
updateCurrentPosition()
|
updateCurrentPosition()
|
||||||
|
|
||||||
|
activity.supportFragmentManager.setFragmentResultListener(
|
||||||
|
ChaptersBottomSheet.SEEK_TO_POSITION_REQUEST_KEY,
|
||||||
|
findViewTreeLifecycleOwner() ?: activity
|
||||||
|
) { _, bundle ->
|
||||||
|
player?.seekTo(bundle.getLong(IntentData.currentPosition))
|
||||||
|
}
|
||||||
|
|
||||||
// enable the chapters dialog in the player
|
// enable the chapters dialog in the player
|
||||||
binding.chapterName.setOnClickListener {
|
binding.chapterName.setOnClickListener {
|
||||||
val sheet = chaptersBottomSheet ?: ChaptersBottomSheet().also {
|
val sheet = chaptersBottomSheet ?: ChaptersBottomSheet()
|
||||||
chaptersBottomSheet = it
|
.apply {
|
||||||
}
|
arguments = bundleOf(
|
||||||
|
IntentData.duration to player?.duration?.div(1000)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.also {
|
||||||
|
chaptersBottomSheet = it
|
||||||
|
}
|
||||||
|
|
||||||
if (sheet.isVisible) {
|
if (sheet.isVisible) {
|
||||||
sheet.dismiss()
|
sheet.dismiss()
|
||||||
@ -231,9 +251,9 @@ abstract class CustomExoPlayerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the name of the video chapter in the exoPlayerView
|
// set the name of the video chapter in the exoPlayerView
|
||||||
fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
|
fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
|
||||||
val player = player ?: return
|
val player = player ?: return
|
||||||
val chapters = getChapters()
|
val chapters = chaptersViewModel.chapters
|
||||||
|
|
||||||
binding.chapterName.isInvisible = chapters.isEmpty()
|
binding.chapterName.isInvisible = chapters.isEmpty()
|
||||||
|
|
||||||
@ -246,10 +266,9 @@ abstract class CustomExoPlayerView(
|
|||||||
// if the user is scrubbing the time bar, don't update
|
// if the user is scrubbing the time bar, don't update
|
||||||
if (scrubbingTimeBar && !forceUpdate) return
|
if (scrubbingTimeBar && !forceUpdate) return
|
||||||
|
|
||||||
val newChapterName =
|
val currentIndex = PlayerHelper.getCurrentChapterIndex(player.currentPosition, chapters)
|
||||||
PlayerHelper.getCurrentChapterIndex(player.currentPosition, chapters)
|
val newChapterName = currentIndex?.let { chapters[it].title.trim() } ?: context.getString(R.string.no_chapter)
|
||||||
?.let { chapters[it].title.trim() }
|
chaptersViewModel.currentChapterIndex.updateIfChanged(currentIndex ?: return)
|
||||||
?: context.getString(R.string.no_chapter)
|
|
||||||
|
|
||||||
// change the chapter name textView text to the chapterName
|
// change the chapter name textView text to the chapterName
|
||||||
if (newChapterName != binding.chapterName.text) {
|
if (newChapterName != binding.chapterName.text) {
|
||||||
@ -627,8 +646,10 @@ abstract class CustomExoPlayerView(
|
|||||||
if (!activity.hasCutout && binding.topBar.marginStart == LANDSCAPE_MARGIN_HORIZONTAL_NONE) return
|
if (!activity.hasCutout && binding.topBar.marginStart == LANDSCAPE_MARGIN_HORIZONTAL_NONE) return
|
||||||
|
|
||||||
// add a margin to the top and the bottom bar in landscape mode for notches
|
// add a margin to the top and the bottom bar in landscape mode for notches
|
||||||
val isForcedPortrait = activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
val isForcedPortrait =
|
||||||
val horizontalMargin = if (isFullscreen() && !isForcedPortrait) LANDSCAPE_MARGIN_HORIZONTAL else LANDSCAPE_MARGIN_HORIZONTAL_NONE
|
activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||||
|
val horizontalMargin =
|
||||||
|
if (isFullscreen() && !isForcedPortrait) LANDSCAPE_MARGIN_HORIZONTAL else LANDSCAPE_MARGIN_HORIZONTAL_NONE
|
||||||
|
|
||||||
listOf(binding.topBar, binding.bottomBar).forEach {
|
listOf(binding.topBar, binding.bottomBar).forEach {
|
||||||
it.updateLayoutParams<MarginLayoutParams> {
|
it.updateLayoutParams<MarginLayoutParams> {
|
||||||
@ -784,23 +805,29 @@ abstract class CustomExoPlayerView(
|
|||||||
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||||
player?.togglePlayPauseState()
|
player?.togglePlayPauseState()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
forward()
|
forward()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
rewind()
|
rewind()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_NAVIGATE_NEXT -> {
|
KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_NAVIGATE_NEXT -> {
|
||||||
PlayingQueue.navigateNext()
|
PlayingQueue.navigateNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_NAVIGATE_PREVIOUS -> {
|
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_NAVIGATE_PREVIOUS -> {
|
||||||
PlayingQueue.navigatePrev()
|
PlayingQueue.navigatePrev()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_F -> {
|
KeyEvent.KEYCODE_F -> {
|
||||||
val fragmentManager = ContextHelper.unwrapActivity(context).supportFragmentManager
|
val fragmentManager = ContextHelper.unwrapActivity(context).supportFragmentManager
|
||||||
fragmentManager.fragments.filterIsInstance<PlayerFragment>().firstOrNull()
|
fragmentManager.fragments.filterIsInstance<PlayerFragment>().firstOrNull()
|
||||||
?.toggleFullscreen()
|
?.toggleFullscreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -826,8 +853,6 @@ abstract class CustomExoPlayerView(
|
|||||||
|
|
||||||
open fun minimizeOrExitPlayer() = Unit
|
open fun minimizeOrExitPlayer() = Unit
|
||||||
|
|
||||||
abstract fun getChapters(): List<ChapterSegment>
|
|
||||||
|
|
||||||
open fun getWindow(): Window = activity.window
|
open fun getWindow(): Window = activity.window
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -31,6 +31,4 @@ class OfflinePlayerView(
|
|||||||
this.chapters = chapters
|
this.chapters = chapters
|
||||||
setCurrentChapterName()
|
setCurrentChapterName()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapters(): List<ChapterSegment> = chapters
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import androidx.media3.common.Player
|
|||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.trackselection.TrackSelector
|
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.obj.ChapterSegment
|
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
@ -281,10 +280,6 @@ class OnlinePlayerView(
|
|||||||
playerOptions?.exitFullscreen()
|
playerOptions?.exitFullscreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapters(): List<ChapterSegment> {
|
|
||||||
return playerViewModel?.chapters.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlaybackEvents(player: Player, events: Player.Events) {
|
override fun onPlaybackEvents(player: Player, events: Player.Events) {
|
||||||
super.onPlaybackEvents(player, events)
|
super.onPlaybackEvents(player, events)
|
||||||
updateDisplayedDuration()
|
updateDisplayedDuration()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user