4.5.2 commit

This commit is contained in:
Xilin Jia 2024-04-03 08:33:11 +00:00
parent 1c97bc3334
commit 40ec9deb06
18 changed files with 585 additions and 872 deletions

View File

@ -21,6 +21,7 @@ Other notable features and changes include:
* A more convenient player control displayed on all pages
* A revamped and more efficient expanded player view showing episode description on the front
* External player class is merged into the player
* New and efficient ways of click and long-click operations on lists:
* click on title area opens the podcast/episode
* long-press on title area automatically enters in selection mode
@ -31,11 +32,8 @@ Other notable features and changes include:
* Left and right swipe actions on lists now have telltales and can be configured on the spot
* Played episodes have clearer markings
* Sort dialog no longer dims the main view
* Play speed setting has been straightened up, three places to set the play speed:
* global setting at the preference
* setting for a feed: either use global or customized
* setting at the player: set for current playing and save for global
* customized feed setting takes precedence when playing an episode
* Play speed setting has been straightened up, three speed can be set separately or combined: current audio, podcast, and global
* customized podcast speed takes precedence when playing an episode
* Added preference "Fast Forward Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 10.0)
* The "Skip to next episode" button on the player
* long-press moves to the next episode

View File

@ -149,8 +149,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020121
versionName "4.5.1"
versionCode 3020122
versionName "4.5.2"
def commit = ""
try {

View File

@ -33,6 +33,7 @@ object PlaybackSpeedUtils {
val feed = item.feed
if (feed?.preferences != null) {
playbackSpeed = feed.preferences!!.feedPlaybackSpeed
Log.d(TAG, "using feed speed $playbackSpeed")
} else {
Log.d(TAG, "Can not get feed specific playback speed: $feed")
}

View File

@ -116,6 +116,10 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
}
}
fun isPlaybackServiceReady() : Boolean {
return playbackService != null
}
private fun unbind() {
try {
activity.unbindService(mConnection)
@ -338,9 +342,9 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
playbackService?.setVideoSurface(holder)
}
fun setPlaybackSpeed(speed: Float) {
fun setPlaybackSpeed(speed: Float, codeArray: Array<Int>? = null) {
if (playbackService != null) {
playbackService!!.setSpeed(speed)
playbackService!!.setSpeed(speed, codeArray)
} else {
EventBus.getDefault().post(SpeedChangedEvent(speed))
}

View File

@ -1617,15 +1617,41 @@ class PlaybackService : MediaBrowserServiceCompat() {
val playable: Playable?
get() = mediaPlayer?.getPlayable()
fun setSpeed(speed: Float) {
fun setSpeed(speed: Float, codeArray: Array<Int>? = null) {
isSpeedForward = false
isFallbackSpeed = false
currentlyPlayingTemporaryPlaybackSpeed = speed
if (currentMediaType == MediaType.VIDEO) {
videoPlaybackSpeed = speed
} else {
setPlaybackSpeed(speed)
if (codeArray != null && codeArray.size == 3) {
if (codeArray[2] == 1) setPlaybackSpeed(speed)
if (codeArray[1] == 1 && playable is FeedMedia) {
var item = (playable as FeedMedia).item
if (item == null) {
val itemId = (playable as FeedMedia).itemId
item = DBReader.getFeedItem(itemId)
}
if (item != null) {
var feed = item.feed
if (feed == null) {
feed = DBReader.getFeed(item.feedId)
}
if (feed != null) {
val feedPreferences = feed.preferences
if (feedPreferences != null) {
feedPreferences.feedPlaybackSpeed = speed
Log.d(TAG, "setSpeed ${feed.title} $speed")
DBWriter.setFeedPreferences(feedPreferences)
EventBus.getDefault().post(
SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id))
}
}
}
}
}
}
mediaPlayer?.setPlaybackParams(speed, isSkipSilence)

View File

@ -340,7 +340,9 @@ class MainActivity : CastEnabledActivity() {
bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom
}
fun setPlayerVisible(visible: Boolean) {
fun setPlayerVisible(visible_: Boolean?) {
val visible = if (visible_ != null) visible_ else if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) false else true
bottomSheet.setLocked(!visible)
if (visible) {
bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state) // Update toolbar visibility
@ -353,7 +355,7 @@ class MainActivity : CastEnabledActivity() {
params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right,
navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0))
mainView.layoutParams = params
val playerView = findViewById<FragmentContainerView>(R.id.playerFragment)
val playerView = findViewById<FragmentContainerView>(R.id.playerFragment1)
val playerParams = playerView.layoutParams as MarginLayoutParams
playerParams.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
playerView.layoutParams = playerParams

View File

@ -15,6 +15,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -28,6 +29,7 @@ import java.text.DecimalFormatSymbols
import java.util.*
open class VariableSpeedDialog : BottomSheetDialogFragment() {
private lateinit var adapter: SpeedSelectionAdapter
private lateinit var speedSeekBar: PlaybackSpeedSeekBar
private lateinit var addCurrentSpeedChip: Chip
@ -38,6 +40,8 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
private var controller: PlaybackController? = null
private val selectedSpeeds: MutableList<Float>
private val settingCode: Array<Int> = Array(3) { 0 }
init {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
@ -46,12 +50,14 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
@UnstableApi override fun onStart() {
super.onStart()
controller = object : PlaybackController(requireActivity()) {
override fun loadMediaInfo() {
if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier))
if (controller == null) {
controller = object : PlaybackController(requireActivity()) {
override fun loadMediaInfo() {
if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier))
}
}
controller?.init()
}
controller?.init()
EventBus.getDefault().register(this)
if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier))
}
@ -73,6 +79,21 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
savedInstanceState: Bundle?
): View? {
_binding = SpeedSelectDialogBinding.inflate(inflater)
val argument = arguments?.getString("default_setting")
when (argument) {
null, "Current" -> {
binding.currentAudio.isChecked = true
}
"Feed" -> {
binding.currentPodcast.isChecked = true
}
else -> {
binding.global.isChecked = true
}
}
speedSeekBar = binding.speedSeekBar
speedSeekBar.setProgressChangedListener { multiplier: Float ->
controller?.setPlaybackSpeed(multiplier)
@ -95,15 +116,24 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
skipSilence.isChecked = isSkipSilence
skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
isSkipSilence = isChecked
controller!!.setSkipSilence(isChecked)
controller?.setSkipSilence(isChecked)
}
return binding.root
}
@OptIn(UnstableApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// if (controller == null || !controller!!.isPlaybackServiceReady()) {
// binding.currentPodcast.visibility = View.INVISIBLE
// } else binding.currentPodcast.visibility = View.VISIBLE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun addCurrentSpeed() {
val newSpeed = speedSeekBar.currentSpeed
if (selectedSpeeds.contains(newSpeed)) {
@ -134,15 +164,16 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
notifyDataSetChanged()
true
}
holder.chip.setOnClickListener {
Handler(Looper.getMainLooper()).postDelayed(
{
if (controller != null) {
dismiss()
controller!!.setPlaybackSpeed(speed)
}
}, 200)
}
holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({
if (binding.currentAudio.isChecked) settingCode[0] = 1
if (binding.currentPodcast.isChecked) settingCode[1] = 1
if (binding.global.isChecked) settingCode[2] = 1
if (controller != null) {
dismiss()
controller!!.setPlaybackSpeed(speed, settingCode)
}
}, 200) }
}
override fun getItemCount(): Int {
@ -156,4 +187,16 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(
chip)
}
companion object {
fun newInstance(argument: String? = null): VariableSpeedDialog {
val dialog = VariableSpeedDialog()
if (argument != null) {
val args = Bundle()
args.putString("default_setting", argument)
dialog.arguments = args
}
return dialog
}
}
}

View File

@ -2,16 +2,23 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.databinding.InternalPlayerFragmentBinding
import ac.mdiq.podcini.feed.util.ImageResourceUtils
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.event.*
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
import ac.mdiq.podcini.playback.event.PlaybackServiceEvent
import ac.mdiq.podcini.playback.event.SleepTimerUpdatedEvent
import ac.mdiq.podcini.playback.event.SpeedChangedEvent
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.service.playback.PlaybackService
import ac.mdiq.podcini.storage.model.feed.Chapter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView
@ -27,7 +34,6 @@ import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.TimeSpeedConverter
import ac.mdiq.podcini.util.event.FavoritesEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
@ -37,15 +43,18 @@ import android.text.Html
import android.util.Log
import android.view.*
import android.widget.ImageButton
import android.widget.ProgressBar
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.widget.Toolbar
import androidx.cardview.widget.CardView
import androidx.core.app.ShareCompat
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.elevation.SurfaceColors
@ -70,35 +79,25 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
var _binding: AudioplayerFragmentBinding? = null
private val binding get() = _binding!!
lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView
lateinit var txtvPlaybackSpeed: TextView
private lateinit var episodeTitle: TextView
private lateinit var itemDesrView: View
private lateinit var txtvPosition: TextView
private lateinit var txtvLength: TextView
private lateinit var sbPosition: ChapterSeekBar
private lateinit var butRev: ImageButton
private lateinit var txtvRev: TextView
private lateinit var butPlay: PlayButton
private lateinit var butFF: ImageButton
private lateinit var txtvFF: TextView
private lateinit var butSkip: ImageButton
private lateinit var txtvSkip: TextView
private lateinit var toolbar: MaterialToolbar
private lateinit var playerFragment: View
private lateinit var progressIndicator: ProgressBar
private var playerFragment1: InternalPlayerFragment? = null
private var playerFragment2: InternalPlayerFragment? = null
private lateinit var playerView1: View
private lateinit var playerView2: View
private lateinit var cardViewSeek: CardView
private lateinit var txtvSeek: TextView
private var controller: PlaybackController? = null
private var disposable: Disposable? = null
private var showTimeLeft = false
private var seekedToChapterStart = false
private var currentChapterIndex = -1
private var duration = 0
private var currentMedia: Playable? = null
@SuppressLint("WrongConstant")
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
@ -120,50 +119,39 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
toolbar.setOnMenuItemClickListener(this)
val externalPlayerFragment = ExternalPlayerFragment()
controller = newPlaybackController()
controller!!.init()
playerFragment1 = InternalPlayerFragment.newInstance(controller!!)
childFragmentManager.beginTransaction()
.replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG)
.replace(R.id.playerFragment1, playerFragment1!!, InternalPlayerFragment.TAG)
.commit()
// playerFragment = binding.playerFragment
playerFragment = binding.root.findViewById(R.id.playerFragment)
playerFragment.setBackgroundColor(
playerView1 = binding.root.findViewById(R.id.playerFragment1)
playerView1.setBackgroundColor(
SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
playerFragment2 = InternalPlayerFragment.newInstance(controller!!)
childFragmentManager.beginTransaction()
.replace(R.id.playerFragment2, playerFragment2!!, InternalPlayerFragment.TAG)
.commit()
playerView2 = binding.root.findViewById(R.id.playerFragment2)
playerView2.setBackgroundColor(
SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
itemDesrView = binding.itemDescription
episodeTitle = binding.titleView
butPlaybackSpeed = binding.butPlaybackSpeed
txtvPlaybackSpeed = binding.txtvPlaybackSpeed
sbPosition = binding.sbPosition
txtvPosition = binding.txtvPosition
txtvLength = binding.txtvLength
butRev = binding.butRev
txtvRev = binding.txtvRev
butPlay = binding.butPlay
butFF = binding.butFF
txtvFF = binding.txtvFF
butSkip = binding.butSkip
txtvSkip = binding.txtvSkip
progressIndicator = binding.progLoading
cardViewSeek = binding.cardViewSeek
txtvSeek = binding.txtvSeek
setupLengthTextView()
setupControlButtons()
butPlaybackSpeed.setOnClickListener {
VariableSpeedDialog().show(childFragmentManager, null)
}
sbPosition.setOnSeekBarChangeListener(this)
val fm = requireActivity().supportFragmentManager
val transaction = fm.beginTransaction()
val itemDescFrag = PlayerDetailsFragment()
transaction.replace(R.id.itemDescription, itemDescFrag).commit()
controller = newPlaybackController()
controller?.init()
loadMediaInfo(false)
// controller = externalPlayerFragment1.controller
// loadMediaInfo(false)
EventBus.getDefault().register(this)
// updateUi(controller?.getMedia())
return binding.root
}
@ -189,62 +177,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
dividerPos[i] = chapters[i].start / duration.toFloat()
}
}
sbPosition.setDividerPos(dividerPos)
}
private fun setupControlButtons() {
butRev.setOnClickListener {
if (controller != null) {
val curr: Int = controller!!.position
controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000)
}
}
butRev.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev)
true
}
butPlay.setOnClickListener {
controller?.init()
controller?.playPause()
}
butPlay.setOnLongClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed)
}
true
}
butFF.setOnClickListener {
if (controller != null) {
val curr: Int = controller!!.position
controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000)
}
}
butFF.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF)
true
}
butSkip.setOnClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) controller!!.speedForward(speedForward)
}
}
butSkip.setOnLongClickListener {
activity?.sendBroadcast(
MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
true
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) {
if (controller == null) return
updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) {
// if (controller == null) return
// updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
// }
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
@ -253,53 +192,62 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
}
private fun setupLengthTextView() {
showTimeLeft = UserPreferences.shouldShowRemainingTime()
txtvLength.setOnClickListener(View.OnClickListener {
if (controller == null) return@OnClickListener
// private fun setupLengthTextView() {
// showTimeLeft = UserPreferences.shouldShowRemainingTime()
// txtvLength.setOnClickListener(View.OnClickListener {
// if (controller == null) return@OnClickListener
//
// showTimeLeft = !showTimeLeft
// UserPreferences.setShowRemainTimeSetting(showTimeLeft)
// updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
// })
// }
showTimeLeft = !showTimeLeft
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
})
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun updatePlaybackSpeedButton(event: SpeedChangedEvent) {
val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
txtvPlaybackSpeed.text = speedStr
butPlaybackSpeed.setSpeed(event.newSpeed)
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// fun updatePlaybackSpeedButton(event: SpeedChangedEvent) {
// val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
// txtvPlaybackSpeed.text = speedStr
// butPlaybackSpeed.setSpeed(event.newSpeed)
// }
private fun loadMediaInfo(includingChapters: Boolean) {
Log.d(TAG, "loadMediaInfo called")
disposable?.dispose()
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
val media: Playable? = controller?.getMedia()
if (media != null) {
if (includingChapters) {
ChapterUtils.loadChapters(media, requireContext(), false)
val theMedia = controller?.getMedia() ?: return
if (currentMedia == null || theMedia?.getIdentifier() != currentMedia?.getIdentifier()) {
Log.d(TAG, "loadMediaInfo loading details")
disposable?.dispose()
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
val media: Playable? = theMedia
if (media != null) {
if (includingChapters) {
ChapterUtils.loadChapters(media, requireContext(), false)
}
emitter.onSuccess(media)
} else {
emitter.onComplete()
}
emitter.onSuccess(media)
} else {
emitter.onComplete()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ media: Playable ->
currentMedia = media
updateUi(media)
playerFragment1?.updateUi(media)
playerFragment2?.updateUi(media)
if (!includingChapters) {
loadMediaInfo(true)
}
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) },
{ updateUi(null) })
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ media: Playable ->
updateUi(media)
if (!includingChapters) {
loadMediaInfo(true)
}
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) },
{ updateUi(null) })
}
private fun newPlaybackController(): PlaybackController {
return object : PlaybackController(requireActivity()) {
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
butPlay.setIsShowPlay(showPlay)
playerFragment1?.butPlay?.setIsShowPlay(showPlay)
playerFragment2?.butPlay?.setIsShowPlay(showPlay)
}
override fun loadMediaInfo() {
@ -307,19 +255,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
override fun onPlaybackEnd() {
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
playerFragment1?.butPlay?.setIsShowPlay(true)
playerFragment2?.butPlay?.setIsShowPlay(true)
(activity as MainActivity).setPlayerVisible(null)
}
}
}
private fun updateUi(media: Playable?) {
if (controller != null) duration = controller!!.duration
if (media == null) return
Log.d(TAG, "updateUi called")
episodeTitle.text = media.getEpisodeTitle()
updatePosition(PlaybackPositionEvent(media.getPosition(), media.getDuration()))
updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)))
setChapterDividers(media)
setupOptionsMenu(media)
}
@ -339,73 +283,33 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onStart() {
super.onStart()
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
if (UserPreferences.speedforwardSpeed > 0.1f) {
txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
} else txtvSkip.visibility = View.GONE
loadMediaInfo(false)
}
override fun onStop() {
super.onStop()
progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
disposable?.dispose()
}
@Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused")
fun bufferUpdate(event: BufferUpdateEvent) {
when {
event.hasStarted() -> {
progressIndicator.visibility = View.VISIBLE
}
event.hasEnded() -> {
progressIndicator.visibility = View.GONE
}
controller != null && controller!!.isStreaming -> {
sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt())
}
else -> {
sbPosition.setSecondaryProgress(0)
}
}
}
@UnstableApi
@Subscribe(threadMode = ThreadMode.MAIN)
fun updatePosition(event: PlaybackPositionEvent) {
if (controller == null) return
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val currentPosition: Int = converter.convert(event.position)
val duration: Int = converter.convert(event.duration)
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt())
currentChapterIndex = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), currentPosition)
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) {
Log.w(TAG, "Could not react to position observer update because of invalid time $currentPosition $duration")
return
}
txtvPosition.text = Converter.getDurationStringLong(currentPosition)
txtvPosition.setContentDescription(getString(R.string.position,
Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong())))
showTimeLeft = UserPreferences.shouldShowRemainingTime()
if (showTimeLeft) {
txtvLength.setContentDescription(getString(R.string.remaining_time,
Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong())))
txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime)
} else {
txtvLength.setContentDescription(getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
txtvLength.text = Converter.getDurationStringLong(duration)
}
if (!sbPosition.isPressed && event.duration > 0) {
val progress: Float = (event.position.toFloat()) / event.duration
sbPosition.progress = (progress * sbPosition.max).toInt()
}
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// @Suppress("unused")
// fun bufferUpdate(event: BufferUpdateEvent) {
// when {
// event.hasStarted() -> {
// progressIndicator.visibility = View.VISIBLE
// }
// event.hasEnded() -> {
// progressIndicator.visibility = View.GONE
// }
//// controller != null && controller!!.isStreaming -> {
//// sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt())
//// }
// else -> {
//// sbPosition.setSecondaryProgress(0)
// }
// }
// }
@Subscribe(threadMode = ThreadMode.MAIN)
fun favoritesChanged(event: FavoritesEvent?) {
@ -427,15 +331,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
var position: Int = converter.convert((prog * controller!!.duration).toInt())
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), position)
if (newChapterIndex > -1) {
if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) {
currentChapterIndex = newChapterIndex
val media = controller!!.getMedia()
position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0
seekedToChapterStart = true
controller!!.seekTo(position)
updateUi(controller!!.getMedia())
sbPosition.highlightCurrentChapter()
}
// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) {
// currentChapterIndex = newChapterIndex
// val media = controller!!.getMedia()
// position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0
// seekedToChapterStart = true
// controller!!.seekTo(position)
// updateUi(controller!!.getMedia())
// sbPosition.highlightCurrentChapter()
// }
txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: (""
+ "\n" + Converter.getDurationStringLong(position))
} else {
@ -538,7 +442,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
fun fadePlayerToToolbar(slideOffset: Float) {
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat()
val player = playerFragment
val player = playerView1
player.alpha = 1 - playerFadeProgress
player.visibility = if (playerFadeProgress > 0.99f) View.INVISIBLE else View.VISIBLE
val toolbarFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.6f).toDouble())) / 0.2f).toFloat()
@ -546,6 +450,297 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
toolbar.visibility = if (toolbarFadeProgress < 0.01f) View.INVISIBLE else View.VISIBLE
}
class InternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private var _binding: InternalPlayerFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var imgvCover: ImageView
lateinit var butPlay: PlayButton
lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView
lateinit var txtvPlaybackSpeed: TextView
private lateinit var episodeTitle: TextView
private lateinit var butRev: ImageButton
private lateinit var txtvRev: TextView
private lateinit var butFF: ImageButton
private lateinit var txtvFF: TextView
private lateinit var butSkip: ImageButton
private lateinit var txtvSkip: TextView
private lateinit var txtvPosition: TextView
private lateinit var txtvLength: TextView
private lateinit var sbPosition: ChapterSeekBar
private var showTimeLeft = false
private var disposable: Disposable? = null
@UnstableApi
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
_binding = InternalPlayerFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
episodeTitle = binding.titleView
butPlaybackSpeed = binding.butPlaybackSpeed
txtvPlaybackSpeed = binding.txtvPlaybackSpeed
imgvCover = binding.imgvCover
butPlay = binding.butPlay
butRev = binding.butRev
txtvRev = binding.txtvRev
butFF = binding.butFF
txtvFF = binding.txtvFF
butSkip = binding.butSkip
txtvSkip = binding.txtvSkip
sbPosition = binding.sbPosition
txtvPosition = binding.txtvPosition
txtvLength = binding.txtvLength
setupLengthTextView()
setupControlButtons()
butPlaybackSpeed.setOnClickListener {
VariableSpeedDialog.newInstance(null).show(childFragmentManager, null)
}
sbPosition.setOnSeekBarChangeListener(this)
binding.internalPlayerFragment.setOnClickListener {
Log.d(TAG, "internalPlayerFragment was clicked")
val media = controller?.getMedia()
if (media != null) {
if (media.getMediaType() == MediaType.AUDIO) {
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else {
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media)
startActivity(intent)
}
}
}
EventBus.getDefault().register(this)
return binding.root
}
@OptIn(UnstableApi::class) override fun onDestroyView() {
super.onDestroyView()
_binding = null
EventBus.getDefault().unregister(this)
}
@UnstableApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
butPlay.setOnClickListener {
if (controller == null) return@setOnClickListener
val media = controller!!.getMedia()
if (media?.getMediaType() == MediaType.VIDEO && controller!!.status != PlayerStatus.PLAYING) {
controller!!.playPause()
requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media))
} else {
controller!!.playPause()
}
}
}
@OptIn(UnstableApi::class) private fun setupControlButtons() {
butRev.setOnClickListener {
if (controller != null) {
val curr: Int = controller!!.position
controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000)
}
}
butRev.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev)
true
}
butPlay.setOnClickListener {
controller?.init()
controller?.playPause()
}
butPlay.setOnLongClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed)
}
true
}
butFF.setOnClickListener {
if (controller != null) {
val curr: Int = controller!!.position
controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000)
}
}
butFF.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF)
true
}
butSkip.setOnClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) controller!!.speedForward(speedForward)
}
}
butSkip.setOnLongClickListener {
activity?.sendBroadcast(
MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
true
}
}
@OptIn(UnstableApi::class) private fun setupLengthTextView() {
showTimeLeft = UserPreferences.shouldShowRemainingTime()
txtvLength.setOnClickListener(View.OnClickListener {
if (controller == null) {
return@OnClickListener
}
showTimeLeft = !showTimeLeft
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
onPositionObserverUpdate(PlaybackPositionEvent(controller!!.position, controller!!.duration))
})
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun updatePlaybackSpeedButton(event: SpeedChangedEvent) {
val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
txtvPlaybackSpeed.text = speedStr
butPlaybackSpeed.setSpeed(event.newSpeed)
}
@UnstableApi
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPositionObserverUpdate(event: PlaybackPositionEvent) {
if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) {
return
}
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val currentPosition: Int = converter.convert(event.position)
val duration: Int = converter.convert(event.duration)
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt())
if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) {
Log.w(TAG, "Could not react to position observer update because of invalid time")
return
}
txtvPosition.text = Converter.getDurationStringLong(currentPosition)
txtvPosition.setContentDescription(getString(R.string.position,
Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong())))
val showTimeLeft = UserPreferences.shouldShowRemainingTime()
if (showTimeLeft) {
txtvLength.setContentDescription(getString(R.string.remaining_time,
Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong())))
txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime)
} else {
txtvLength.setContentDescription(getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
txtvLength.text = Converter.getDurationStringLong(duration)
}
if (!sbPosition.isPressed) {
val progress: Float = (event.position.toFloat()) / event.duration
sbPosition.progress = (progress * sbPosition.max).toInt()
}
}
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
when (event.action) {
PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> {
(activity as MainActivity).setPlayerVisible(false)
}
PlaybackServiceEvent.Action.SERVICE_STARTED -> {
(activity as MainActivity).setPlayerVisible(true)
}
}
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Fragment is about to be destroyed")
disposable?.dispose()
}
@OptIn(UnstableApi::class) override fun onStart() {
super.onStart()
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
if (UserPreferences.speedforwardSpeed > 0.1f) {
txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
} else txtvSkip.visibility = View.GONE
val media = controller?.getMedia() ?: return
updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)))
}
@UnstableApi
override fun onPause() {
super.onPause()
controller?.pause()
}
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
if (controller != null) {
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
controller!!.seekTo((prog * controller!!.duration).toInt())
}
}
@UnstableApi
fun updateUi(media: Playable?) {
if (media == null) return
Log.d(TAG, "updateUi called")
episodeTitle.text = media.getEpisodeTitle()
(activity as MainActivity).setPlayerVisible(true)
onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration()))
val options = RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.fitCenter()
.dontAnimate()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
when {
!imgLoc.isNullOrBlank() -> Glide.with(this)
.load(imgLoc)
.apply(options)
.into(imgvCover)
!imgLocFB.isNullOrBlank() -> Glide.with(this)
.load(imgLocFB)
.apply(options)
.into(imgvCover)
else -> imgvCover.setImageResource(R.mipmap.ic_launcher)
}
if (controller?.isPlayingVideoLocally == true) {
(activity as MainActivity).bottomSheet.setLocked(true)
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
} else {
butPlay.visibility = View.VISIBLE
(activity as MainActivity).bottomSheet.setLocked(false)
}
}
companion object {
const val TAG: String = "InternalPlayerFragment"
var controller: PlaybackController? = null
fun newInstance(controller_: PlaybackController) : InternalPlayerFragment {
controller = controller_
return InternalPlayerFragment()
}
}
}
companion object {
const val TAG: String = "AudioPlayerFragment"
}

View File

@ -1,387 +0,0 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ExternalPlayerFragmentBinding
import ac.mdiq.podcini.feed.util.ImageResourceUtils.getEpisodeListImageLocation
import ac.mdiq.podcini.feed.util.ImageResourceUtils.getFallbackImageLocation
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
import ac.mdiq.podcini.playback.event.PlaybackServiceEvent
import ac.mdiq.podcini.playback.event.SpeedChangedEvent
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.service.playback.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.view.ChapterSeekBar
import ac.mdiq.podcini.ui.view.PlayButton
import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.TimeSpeedConverter
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.bottomsheet.BottomSheetBehavior
import io.reactivex.Maybe
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.text.DecimalFormat
import java.text.NumberFormat
import kotlin.math.max
/**
* Fragment which is supposed to be displayed outside of the MediaplayerActivity.
*/
class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private var _binding: ExternalPlayerFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var imgvCover: ImageView
private lateinit var butPlay: PlayButton
lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView
lateinit var txtvPlaybackSpeed: TextView
private lateinit var episodeTitle: TextView
private lateinit var butRev: ImageButton
private lateinit var txtvRev: TextView
private lateinit var butFF: ImageButton
private lateinit var txtvFF: TextView
private lateinit var butSkip: ImageButton
private lateinit var txtvSkip: TextView
private lateinit var txtvPosition: TextView
private lateinit var txtvLength: TextView
private lateinit var sbPosition: ChapterSeekBar
private var showTimeLeft = false
private var currentMedia: Playable? = null
private var controller: PlaybackController? = null
private var disposable: Disposable? = null
@UnstableApi
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
_binding = ExternalPlayerFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
episodeTitle = binding.titleView
butPlaybackSpeed = binding.butPlaybackSpeed
txtvPlaybackSpeed = binding.txtvPlaybackSpeed
imgvCover = binding.imgvCover
butPlay = binding.butPlay
butRev = binding.butRev
txtvRev = binding.txtvRev
butFF = binding.butFF
txtvFF = binding.txtvFF
butSkip = binding.butSkip
txtvSkip = binding.txtvSkip
sbPosition = binding.sbPosition
txtvPosition = binding.txtvPosition
txtvLength = binding.txtvLength
setupLengthTextView()
setupControlButtons()
butPlaybackSpeed.setOnClickListener {
VariableSpeedDialog().show(childFragmentManager, null)
}
sbPosition.setOnSeekBarChangeListener(this)
binding.externalPlayerFragment.setOnClickListener {
Log.d(TAG, "externalPlayerFragment was clicked")
val media = controller?.getMedia()
if (media != null) {
if (media.getMediaType() == MediaType.AUDIO) {
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else {
val intent = getPlayerActivityIntent(requireContext(), media)
startActivity(intent)
}
}
}
controller = setupPlaybackController()
controller!!.init()
EventBus.getDefault().register(this)
return binding.root
}
@OptIn(UnstableApi::class) override fun onDestroyView() {
super.onDestroyView()
_binding = null
controller?.release()
controller = null
EventBus.getDefault().unregister(this)
}
@UnstableApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
butPlay.setOnClickListener {
if (controller == null) return@setOnClickListener
val media = controller!!.getMedia()
if (media?.getMediaType() == MediaType.VIDEO && controller!!.status != PlayerStatus.PLAYING) {
controller!!.playPause()
requireContext().startActivity(getPlayerActivityIntent(requireContext(), media))
} else {
controller!!.playPause()
}
}
loadMediaInfo()
}
@OptIn(UnstableApi::class) private fun setupControlButtons() {
butRev.setOnClickListener {
if (controller != null) {
val curr: Int = controller!!.position
controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000)
}
}
butRev.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev)
true
}
butPlay.setOnClickListener {
controller?.init()
controller?.playPause()
}
butPlay.setOnLongClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed)
}
true
}
butFF.setOnClickListener {
if (controller != null) {
val curr: Int = controller!!.position
controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000)
}
}
butFF.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF)
true
}
butSkip.setOnClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) controller!!.speedForward(speedForward)
}
}
butSkip.setOnLongClickListener {
activity?.sendBroadcast(
MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
true
}
}
@UnstableApi
private fun setupPlaybackController(): PlaybackController {
return object : PlaybackController(requireActivity()) {
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
butPlay.setIsShowPlay(showPlay)
}
override fun loadMediaInfo() {
Log.d(TAG, "setupPlaybackController loadMediaInfo called")
this@ExternalPlayerFragment.loadMediaInfo()
}
override fun onPlaybackEnd() {
(activity as MainActivity).setPlayerVisible(false)
}
}
}
@OptIn(UnstableApi::class) private fun setupLengthTextView() {
showTimeLeft = UserPreferences.shouldShowRemainingTime()
txtvLength.setOnClickListener(View.OnClickListener {
if (controller == null) {
return@OnClickListener
}
showTimeLeft = !showTimeLeft
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
onPositionObserverUpdate(PlaybackPositionEvent(controller!!.position, controller!!.duration))
})
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun updatePlaybackSpeedButton(event: SpeedChangedEvent) {
val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
txtvPlaybackSpeed.text = speedStr
butPlaybackSpeed.setSpeed(event.newSpeed)
}
@UnstableApi
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPositionObserverUpdate(event: PlaybackPositionEvent) {
if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) {
return
}
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val currentPosition: Int = converter.convert(event.position)
val duration: Int = converter.convert(event.duration)
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt())
if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) {
Log.w(TAG, "Could not react to position observer update because of invalid time")
return
}
txtvPosition.text = Converter.getDurationStringLong(currentPosition)
txtvPosition.setContentDescription(getString(R.string.position,
Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong())))
val showTimeLeft = UserPreferences.shouldShowRemainingTime()
if (showTimeLeft) {
txtvLength.setContentDescription(getString(R.string.remaining_time,
Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong())))
txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime)
} else {
txtvLength.setContentDescription(getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
txtvLength.text = Converter.getDurationStringLong(duration)
}
if (!sbPosition.isPressed) {
val progress: Float = (event.position.toFloat()) / event.duration
sbPosition.progress = (progress * sbPosition.max).toInt()
}
}
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
when (event.action) {
PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> {
(activity as MainActivity).setPlayerVisible(false)
}
PlaybackServiceEvent.Action.SERVICE_STARTED -> {
(activity as MainActivity).setPlayerVisible(true)
}
}
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Fragment is about to be destroyed")
disposable?.dispose()
}
@OptIn(UnstableApi::class) override fun onStart() {
super.onStart()
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
if (UserPreferences.speedforwardSpeed > 0.1f) {
txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
} else txtvSkip.visibility = View.GONE
val media = controller?.getMedia() ?: return
updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)))
}
@UnstableApi
override fun onPause() {
super.onPause()
controller?.pause()
}
@UnstableApi
private fun loadMediaInfo() {
Log.d(TAG, "loadMediaInfo called")
if (controller == null) {
Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!")
return
}
val theMedia = controller?.getMedia()
if (currentMedia == null || theMedia?.getIdentifier() != currentMedia?.getIdentifier()) {
Log.d(TAG, "reloading media info")
disposable?.dispose()
disposable = Maybe.fromCallable<Playable?> { theMedia }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ media: Playable? ->
currentMedia = media
this.updateUi(media) },
{ error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) },
{
// (activity as MainActivity).setPlayerVisible(false)
})
}
}
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
if (controller != null) {
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
controller!!.seekTo((prog * controller!!.duration).toInt())
}
}
@UnstableApi
private fun updateUi(media: Playable?) {
if (media == null) return
Log.d(TAG, "updateUi called")
episodeTitle.text = media.getEpisodeTitle()
(activity as MainActivity).setPlayerVisible(true)
onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration()))
val options = RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.fitCenter()
.dontAnimate()
val imgLoc = getEpisodeListImageLocation(media)
val imgLocFB = getFallbackImageLocation(media)
when {
!imgLoc.isNullOrBlank() -> Glide.with(this)
.load(imgLoc)
.apply(options)
.into(imgvCover)
!imgLocFB.isNullOrBlank() -> Glide.with(this)
.load(imgLocFB)
.apply(options)
.into(imgvCover)
else -> imgvCover.setImageResource(R.mipmap.ic_launcher)
}
if (controller?.isPlayingVideoLocally == true) {
(activity as MainActivity).bottomSheet.setLocked(true)
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
} else {
butPlay.visibility = View.VISIBLE
(activity as MainActivity).bottomSheet.setLocked(false)
}
}
companion object {
const val TAG: String = "ExternalPlayerFragment"
}
}

View File

@ -204,11 +204,9 @@ class FeedSettingsFragment : Fragment() {
val feedPlaybackSpeedPreference = findPreference<Preference>(PREF_FEED_PLAYBACK_SPEED)
feedPlaybackSpeedPreference!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val viewBinding =
PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater)
val viewBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater)
viewBinding.seekBar.setProgressChangedListener { speed: Float? ->
viewBinding.currentSpeedLabel.text = String.format(
Locale.getDefault(), "%.2fx", speed)
viewBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed)
}
viewBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
viewBinding.seekBar.isEnabled = !isChecked
@ -224,8 +222,8 @@ class FeedSettingsFragment : Fragment() {
.setTitle(R.string.playback_speed)
.setView(viewBinding.root)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val newSpeed = if (viewBinding.useGlobalCheckbox.isChecked
) FeedPreferences.SPEED_USE_GLOBAL else viewBinding.seekBar.currentSpeed
val newSpeed = if (viewBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
else viewBinding.seekBar.currentSpeed
feedPreferences!!.feedPlaybackSpeed = newSpeed
if (feedPreferences != null) DBWriter.setFeedPreferences(feedPreferences!!)
EventBus.getDefault().post(

View File

@ -10,9 +10,7 @@ import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.storage.model.feed.SortOrder
import ac.mdiq.podcini.storage.model.feed.*
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.QueueRecyclerAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
@ -29,6 +27,7 @@ import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder
import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.FeedItemUtil
import ac.mdiq.podcini.util.event.*
import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
@ -311,6 +310,18 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
refreshToolbarState()
}
// @Subscribe(threadMode = ThreadMode.MAIN)
// @Suppress("unused")
// fun speedPresetChanged(event: SpeedPresetChangedEvent) {
//// Log.d(TAG,"speedPresetChanged called")
//// for (item in queue) {
//// if (item.feed?.id == event.feedId && item.feed!!.preferences != null) {
//// Log.d(TAG, "speedPresetChanged ${item.feed!!.title} ${event.speed}")
//// item.feed!!.preferences!!.feedPlaybackSpeed = event.speed
//// }
//// }
// }
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) {
refreshSwipeTelltale()

View File

@ -39,7 +39,7 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
findPreference<Preference>(PREF_PLAYBACK_SPEED_LAUNCHER)!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
VariableSpeedDialog().show(childFragmentManager, null)
VariableSpeedDialog.newInstance("Global").show(childFragmentManager, null)
true
}
findPreference<Preference>(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER)!!.onPreferenceClickListener =

View File

@ -8,7 +8,7 @@
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/playerFragment"
android:id="@+id/playerFragment1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
@ -35,7 +35,7 @@
android:id="@+id/itemDescription"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_above="@id/playtime_layout"
android:layout_above="@id/playerFragment2"
android:layout_below="@id/toolbar"
android:layout_marginBottom="12dp" />
@ -75,231 +75,13 @@
tools:text="1:06:29" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:id="@+id/playtime_layout"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/playerFragment2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_alignParentBottom="true"
android:layoutDirection="ltr"
android:orientation="vertical">
<TextView
android:id="@+id/title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:gravity="start"
android:text="Title"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:textSize="15sp"/>
<ac.mdiq.podcini.ui.view.ChapterSeekBar
android:id="@+id/sbPosition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:clickable="true"
android:max="500"
tools:progress="100"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<ac.mdiq.podcini.ui.view.NoRelayoutTextView
android:id="@+id/txtvPosition"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="@string/position_default_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_micro"/>
<ac.mdiq.podcini.ui.view.NoRelayoutTextView
android:id="@+id/txtvLength"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:textAlignment="textEnd"
android:background="?android:attr/selectableItemBackground"
android:text="@string/position_default_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_micro"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/player_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<ac.mdiq.podcini.ui.view.PlayButton
android:id="@+id/butPlay"
android:layout_width="@dimen/audioplayer_playercontrols_length_big"
android:layout_height="@dimen/audioplayer_playercontrols_length_big"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pause_label"
android:padding="8dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_play_48dp"
tools:srcCompat="@drawable/ic_play_48dp" />
<ac.mdiq.podcini.ui.common.CircularProgressBar
android:layout_width="@dimen/audioplayer_playercontrols_length_big"
android:layout_height="@dimen/audioplayer_playercontrols_length_big"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:foregroundColor="?attr/action_icon_color" />
<ProgressBar
android:id="@+id/progLoading"
android:layout_width="@dimen/audioplayer_playercontrols_length_big"
android:layout_height="@dimen/audioplayer_playercontrols_length_big"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:visibility="gone"
style="?android:attr/progressBarStyle" />
<ImageButton
android:id="@+id/butRev"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
android:layout_toStartOf="@id/butPlay"
android:layout_toLeftOf="@id/butPlay"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/rewind_label"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fast_rewind"
tools:srcCompat="@drawable/ic_fast_rewind" />
<TextView
android:id="@+id/txtvRev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butRev"
android:layout_alignStart="@id/butRev"
android:layout_alignLeft="@id/butRev"
android:layout_alignEnd="@id/butRev"
android:layout_alignRight="@id/butRev"
android:clickable="false"
android:gravity="center"
android:text="30"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView
android:id="@+id/butPlaybackSpeed"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/butRev"
android:layout_toLeftOf="@id/butRev"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/playback_speed"
app:foregroundColor="?attr/action_icon_color"
tools:srcCompat="@drawable/ic_playback_speed" />
<TextView
android:id="@+id/txtvPlaybackSpeed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butPlaybackSpeed"
android:layout_alignStart="@id/butPlaybackSpeed"
android:layout_alignLeft="@id/butPlaybackSpeed"
android:layout_alignEnd="@id/butPlaybackSpeed"
android:layout_alignRight="@id/butPlaybackSpeed"
android:clickable="false"
android:gravity="center"
android:text="1.00"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<ImageButton
android:id="@+id/butFF"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
android:layout_toEndOf="@id/butPlay"
android:layout_toRightOf="@id/butPlay"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/fast_forward_label"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fast_forward"
tools:srcCompat="@drawable/ic_fast_forward" />
<TextView
android:id="@+id/txtvFF"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butFF"
android:layout_alignStart="@id/butFF"
android:layout_alignLeft="@id/butFF"
android:layout_alignEnd="@id/butFF"
android:layout_alignRight="@id/butFF"
android:clickable="false"
android:gravity="center"
android:text="30"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<ImageButton
android:id="@+id/butSkip"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/butFF"
android:layout_toRightOf="@id/butFF"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/skip_episode_label"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_skip_48dp"
tools:srcCompat="@drawable/ic_skip_48dp" />
<TextView
android:id="@+id/txtvSkip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butSkip"
android:layout_alignStart="@id/butSkip"
android:layout_alignLeft="@id/butSkip"
android:layout_alignEnd="@id/butSkip"
android:layout_alignRight="@id/butSkip"
android:clickable="false"
android:gravity="center"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>
tools:layout_height="@dimen/external_player_height" />
</RelativeLayout>

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/external_player_fragment"
android:id="@+id/internal_player_fragment"
android:layout_width="match_parent"
android:layout_height="@dimen/external_player_height"
android:background="?attr/selectableItemBackground"
@ -25,7 +25,7 @@
android:id="@+id/sbPosition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5.dp"
android:layout_marginTop="5dp"
android:clickable="true"
android:max="500"
tools:progress="100" />
@ -126,6 +126,7 @@
android:layout_alignLeft="@id/butRev"
android:layout_alignEnd="@id/butRev"
android:layout_alignRight="@id/butRev"
android:layout_marginBottom="5dp"
android:clickable="false"
android:gravity="center"
android:text="30"
@ -155,6 +156,7 @@
android:layout_alignLeft="@id/butPlaybackSpeed"
android:layout_alignEnd="@id/butPlaybackSpeed"
android:layout_alignRight="@id/butPlaybackSpeed"
android:layout_marginBottom="5dp"
android:clickable="false"
android:gravity="center"
android:text="1.00"
@ -186,6 +188,7 @@
android:layout_alignLeft="@id/butFF"
android:layout_alignEnd="@id/butFF"
android:layout_alignRight="@id/butFF"
android:layout_marginBottom="5dp"
android:clickable="false"
android:gravity="center"
android:text="30"
@ -217,6 +220,7 @@
android:layout_alignLeft="@id/butSkip"
android:layout_alignEnd="@id/butSkip"
android:layout_alignRight="@id/butSkip"
android:layout_marginBottom="5dp"
android:clickable="false"
android:gravity="center"
android:textColor="?android:attr/textColorSecondary"

View File

@ -58,12 +58,33 @@
android:text="@string/All" />
</RadioGroup>
<TextView
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/speed_presets"
style="@style/Podcini.TextView.ListItemPrimaryTitle" />
android:orientation="horizontal">
<CheckBox
android:id="@+id/currentAudio"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:checked="true"
android:text="@string/current_episode" />
<CheckBox
android:id="@+id/currentPodcast"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/current_podcast" />
<CheckBox
android:id="@+id/global"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/global" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/selected_speeds_grid"

View File

@ -27,6 +27,9 @@
<string name="years_statistics_label">Years</string>
<string name="notification_pref_fragment">Notifications</string>
<string name="current_playing_episode">Current</string>
<string name="current_episode">Current</string>
<string name="current_podcast">Podcast</string>
<string name="global">Global</string>
<string name="podcini_echo" translatable="false">Podcini Echo</string>
<string name="podcini_echo_year" translatable="false">Podcini Echo %d</string>

View File

@ -202,4 +202,10 @@
## 4.5.1
* fixed bug in subscription sorting
* fixed bug in subscription sorting
## 4.5.2
* revamped audio player class, merged external player in
* speed setting now allows setting with three options: current audio, podcast, and global.
* added a bit bottom margin for the numbers in player

View File

@ -0,0 +1,6 @@
Version 4.5.2 brings several changes:
* revamped audio player class, merged external player in
* speed setting now allows setting with three options: current audio, podcast, and global.
* added a bit bottom margin for the numbers in player