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 more convenient player control displayed on all pages
* A revamped and more efficient expanded player view showing episode description on the front * 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: * New and efficient ways of click and long-click operations on lists:
* click on title area opens the podcast/episode * click on title area opens the podcast/episode
* long-press on title area automatically enters in selection mode * 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 * Left and right swipe actions on lists now have telltales and can be configured on the spot
* Played episodes have clearer markings * Played episodes have clearer markings
* Sort dialog no longer dims the main view * Sort dialog no longer dims the main view
* Play speed setting has been straightened up, three places to set the play speed: * Play speed setting has been straightened up, three speed can be set separately or combined: current audio, podcast, and global
* global setting at the preference * customized podcast speed takes precedence when playing an episode
* 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
* 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) * 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 * The "Skip to next episode" button on the player
* long-press moves to the next episode * long-press moves to the next episode

View File

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

View File

@ -33,6 +33,7 @@ object PlaybackSpeedUtils {
val feed = item.feed val feed = item.feed
if (feed?.preferences != null) { if (feed?.preferences != null) {
playbackSpeed = feed.preferences!!.feedPlaybackSpeed playbackSpeed = feed.preferences!!.feedPlaybackSpeed
Log.d(TAG, "using feed speed $playbackSpeed")
} else { } else {
Log.d(TAG, "Can not get feed specific playback speed: $feed") 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() { private fun unbind() {
try { try {
activity.unbindService(mConnection) activity.unbindService(mConnection)
@ -338,9 +342,9 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
playbackService?.setVideoSurface(holder) playbackService?.setVideoSurface(holder)
} }
fun setPlaybackSpeed(speed: Float) { fun setPlaybackSpeed(speed: Float, codeArray: Array<Int>? = null) {
if (playbackService != null) { if (playbackService != null) {
playbackService!!.setSpeed(speed) playbackService!!.setSpeed(speed, codeArray)
} else { } else {
EventBus.getDefault().post(SpeedChangedEvent(speed)) EventBus.getDefault().post(SpeedChangedEvent(speed))
} }

View File

@ -1617,15 +1617,41 @@ class PlaybackService : MediaBrowserServiceCompat() {
val playable: Playable? val playable: Playable?
get() = mediaPlayer?.getPlayable() get() = mediaPlayer?.getPlayable()
fun setSpeed(speed: Float) { fun setSpeed(speed: Float, codeArray: Array<Int>? = null) {
isSpeedForward = false isSpeedForward = false
isFallbackSpeed = false isFallbackSpeed = false
currentlyPlayingTemporaryPlaybackSpeed = speed currentlyPlayingTemporaryPlaybackSpeed = speed
if (currentMediaType == MediaType.VIDEO) { if (currentMediaType == MediaType.VIDEO) {
videoPlaybackSpeed = speed videoPlaybackSpeed = speed
} else { } 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) mediaPlayer?.setPlaybackParams(speed, isSkipSilence)

View File

@ -340,7 +340,9 @@ class MainActivity : CastEnabledActivity() {
bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom 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) bottomSheet.setLocked(!visible)
if (visible) { if (visible) {
bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state) // Update toolbar visibility bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state) // Update toolbar visibility
@ -353,7 +355,7 @@ class MainActivity : CastEnabledActivity() {
params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right,
navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0)) navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0))
mainView.layoutParams = params mainView.layoutParams = params
val playerView = findViewById<FragmentContainerView>(R.id.playerFragment) val playerView = findViewById<FragmentContainerView>(R.id.playerFragment1)
val playerParams = playerView.layoutParams as MarginLayoutParams val playerParams = playerView.layoutParams as MarginLayoutParams
playerParams.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0) playerParams.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
playerView.layoutParams = playerParams playerView.layoutParams = playerParams

View File

@ -15,6 +15,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -28,6 +29,7 @@ import java.text.DecimalFormatSymbols
import java.util.* import java.util.*
open class VariableSpeedDialog : BottomSheetDialogFragment() { open class VariableSpeedDialog : BottomSheetDialogFragment() {
private lateinit var adapter: SpeedSelectionAdapter private lateinit var adapter: SpeedSelectionAdapter
private lateinit var speedSeekBar: PlaybackSpeedSeekBar private lateinit var speedSeekBar: PlaybackSpeedSeekBar
private lateinit var addCurrentSpeedChip: Chip private lateinit var addCurrentSpeedChip: Chip
@ -38,6 +40,8 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
private var controller: PlaybackController? = null private var controller: PlaybackController? = null
private val selectedSpeeds: MutableList<Float> private val selectedSpeeds: MutableList<Float>
private val settingCode: Array<Int> = Array(3) { 0 }
init { init {
val format = DecimalFormatSymbols(Locale.US) val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.' format.decimalSeparator = '.'
@ -46,12 +50,14 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
@UnstableApi override fun onStart() { @UnstableApi override fun onStart() {
super.onStart() super.onStart()
controller = object : PlaybackController(requireActivity()) { if (controller == null) {
override fun loadMediaInfo() { controller = object : PlaybackController(requireActivity()) {
if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) override fun loadMediaInfo() {
if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier))
}
} }
controller?.init()
} }
controller?.init()
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier))
} }
@ -73,6 +79,21 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = SpeedSelectDialogBinding.inflate(inflater) _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 = binding.speedSeekBar
speedSeekBar.setProgressChangedListener { multiplier: Float -> speedSeekBar.setProgressChangedListener { multiplier: Float ->
controller?.setPlaybackSpeed(multiplier) controller?.setPlaybackSpeed(multiplier)
@ -95,15 +116,24 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
skipSilence.isChecked = isSkipSilence skipSilence.isChecked = isSkipSilence
skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
isSkipSilence = isChecked isSkipSilence = isChecked
controller!!.setSkipSilence(isChecked) controller?.setSkipSilence(isChecked)
} }
return binding.root 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() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
} }
private fun addCurrentSpeed() { private fun addCurrentSpeed() {
val newSpeed = speedSeekBar.currentSpeed val newSpeed = speedSeekBar.currentSpeed
if (selectedSpeeds.contains(newSpeed)) { if (selectedSpeeds.contains(newSpeed)) {
@ -134,15 +164,16 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
notifyDataSetChanged() notifyDataSetChanged()
true true
} }
holder.chip.setOnClickListener { holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper()).postDelayed( if (binding.currentAudio.isChecked) settingCode[0] = 1
{ if (binding.currentPodcast.isChecked) settingCode[1] = 1
if (controller != null) { if (binding.global.isChecked) settingCode[2] = 1
dismiss()
controller!!.setPlaybackSpeed(speed) if (controller != null) {
} dismiss()
}, 200) controller!!.setPlaybackSpeed(speed, settingCode)
} }
}, 200) }
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
@ -156,4 +187,16 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder( inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(
chip) 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.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding 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.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.playback.PlaybackController import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity 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.preferences.UserPreferences
import ac.mdiq.podcini.receiver.MediaButtonReceiver 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.Chapter
import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia 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.storage.model.playback.Playable
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView 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.TimeSpeedConverter
import ac.mdiq.podcini.util.event.FavoritesEvent import ac.mdiq.podcini.util.event.FavoritesEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent import ac.mdiq.podcini.util.event.PlayerErrorEvent
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
@ -37,15 +43,18 @@ import android.text.Html
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ProgressBar import android.widget.ImageView
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.media3.common.util.UnstableApi 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.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
@ -70,35 +79,25 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
var _binding: AudioplayerFragmentBinding? = null var _binding: AudioplayerFragmentBinding? = null
private val binding get() = _binding!! 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 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 lateinit var toolbar: MaterialToolbar
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 cardViewSeek: CardView
private lateinit var txtvSeek: TextView private lateinit var txtvSeek: TextView
private var controller: PlaybackController? = null private var controller: PlaybackController? = null
private var disposable: Disposable? = null private var disposable: Disposable? = null
private var showTimeLeft = false
private var seekedToChapterStart = false private var seekedToChapterStart = false
private var currentChapterIndex = -1 private var currentChapterIndex = -1
private var duration = 0 private var duration = 0
private var currentMedia: Playable? = null
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
override fun onCreateView(inflater: LayoutInflater, override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -120,50 +119,39 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
val externalPlayerFragment = ExternalPlayerFragment() controller = newPlaybackController()
controller!!.init()
playerFragment1 = InternalPlayerFragment.newInstance(controller!!)
childFragmentManager.beginTransaction() childFragmentManager.beginTransaction()
.replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG) .replace(R.id.playerFragment1, playerFragment1!!, InternalPlayerFragment.TAG)
.commit() .commit()
// playerFragment = binding.playerFragment playerView1 = binding.root.findViewById(R.id.playerFragment1)
playerFragment = binding.root.findViewById(R.id.playerFragment) playerView1.setBackgroundColor(
playerFragment.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)) SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
itemDesrView = binding.itemDescription 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 cardViewSeek = binding.cardViewSeek
txtvSeek = binding.txtvSeek txtvSeek = binding.txtvSeek
setupLengthTextView()
setupControlButtons()
butPlaybackSpeed.setOnClickListener {
VariableSpeedDialog().show(childFragmentManager, null)
}
sbPosition.setOnSeekBarChangeListener(this)
val fm = requireActivity().supportFragmentManager val fm = requireActivity().supportFragmentManager
val transaction = fm.beginTransaction() val transaction = fm.beginTransaction()
val itemDescFrag = PlayerDetailsFragment() val itemDescFrag = PlayerDetailsFragment()
transaction.replace(R.id.itemDescription, itemDescFrag).commit() transaction.replace(R.id.itemDescription, itemDescFrag).commit()
controller = newPlaybackController() // controller = externalPlayerFragment1.controller
controller?.init() // loadMediaInfo(false)
loadMediaInfo(false)
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
// updateUi(controller?.getMedia())
return binding.root return binding.root
} }
@ -189,62 +177,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
dividerPos[i] = chapters[i].start / duration.toFloat() dividerPos[i] = chapters[i].start / duration.toFloat()
} }
} }
sbPosition.setDividerPos(dividerPos)
} }
private fun setupControlButtons() { // @Subscribe(threadMode = ThreadMode.MAIN)
butRev.setOnClickListener { // fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) {
if (controller != null) { // if (controller == null) return
val curr: Int = controller!!.position // updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
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) @Subscribe(threadMode = ThreadMode.MAIN)
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
@ -253,53 +192,62 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
} }
private fun setupLengthTextView() { // private fun setupLengthTextView() {
showTimeLeft = UserPreferences.shouldShowRemainingTime() // showTimeLeft = UserPreferences.shouldShowRemainingTime()
txtvLength.setOnClickListener(View.OnClickListener { // txtvLength.setOnClickListener(View.OnClickListener {
if (controller == null) return@OnClickListener // if (controller == null) return@OnClickListener
//
// showTimeLeft = !showTimeLeft
// UserPreferences.setShowRemainTimeSetting(showTimeLeft)
// updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
// })
// }
showTimeLeft = !showTimeLeft // @Subscribe(threadMode = ThreadMode.MAIN)
UserPreferences.setShowRemainTimeSetting(showTimeLeft) // fun updatePlaybackSpeedButton(event: SpeedChangedEvent) {
updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration)) // 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) { private fun loadMediaInfo(includingChapters: Boolean) {
Log.d(TAG, "loadMediaInfo called") Log.d(TAG, "loadMediaInfo called")
disposable?.dispose()
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> -> val theMedia = controller?.getMedia() ?: return
val media: Playable? = controller?.getMedia() if (currentMedia == null || theMedia?.getIdentifier() != currentMedia?.getIdentifier()) {
if (media != null) { Log.d(TAG, "loadMediaInfo loading details")
if (includingChapters) { disposable?.dispose()
ChapterUtils.loadChapters(media, requireContext(), false) 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 { private fun newPlaybackController(): PlaybackController {
return object : PlaybackController(requireActivity()) { return object : PlaybackController(requireActivity()) {
override fun updatePlayButtonShowsPlay(showPlay: Boolean) { override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
butPlay.setIsShowPlay(showPlay) playerFragment1?.butPlay?.setIsShowPlay(showPlay)
playerFragment2?.butPlay?.setIsShowPlay(showPlay)
} }
override fun loadMediaInfo() { override fun loadMediaInfo() {
@ -307,19 +255,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
override fun onPlaybackEnd() { 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?) { private fun updateUi(media: Playable?) {
if (controller != null) duration = controller!!.duration
if (media == null) return
Log.d(TAG, "updateUi called") Log.d(TAG, "updateUi called")
episodeTitle.text = media.getEpisodeTitle()
updatePosition(PlaybackPositionEvent(media.getPosition(), media.getDuration()))
updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)))
setChapterDividers(media) setChapterDividers(media)
setupOptionsMenu(media) setupOptionsMenu(media)
} }
@ -339,73 +283,33 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) loadMediaInfo(false)
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
} }
override fun onStop() { override fun onStop() {
super.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() disposable?.dispose()
} }
@Subscribe(threadMode = ThreadMode.MAIN) // @Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused") // @Suppress("unused")
fun bufferUpdate(event: BufferUpdateEvent) { // fun bufferUpdate(event: BufferUpdateEvent) {
when { // when {
event.hasStarted() -> { // event.hasStarted() -> {
progressIndicator.visibility = View.VISIBLE // progressIndicator.visibility = View.VISIBLE
} // }
event.hasEnded() -> { // event.hasEnded() -> {
progressIndicator.visibility = View.GONE // progressIndicator.visibility = View.GONE
} // }
controller != null && controller!!.isStreaming -> { //// controller != null && controller!!.isStreaming -> {
sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt()) //// sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt())
} //// }
else -> { // else -> {
sbPosition.setSecondaryProgress(0) //// 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) @Subscribe(threadMode = ThreadMode.MAIN)
fun favoritesChanged(event: FavoritesEvent?) { fun favoritesChanged(event: FavoritesEvent?) {
@ -427,15 +331,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
var position: Int = converter.convert((prog * controller!!.duration).toInt()) var position: Int = converter.convert((prog * controller!!.duration).toInt())
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), position) val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), position)
if (newChapterIndex > -1) { if (newChapterIndex > -1) {
if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) { // if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) {
currentChapterIndex = newChapterIndex // currentChapterIndex = newChapterIndex
val media = controller!!.getMedia() // val media = controller!!.getMedia()
position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0 // position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0
seekedToChapterStart = true // seekedToChapterStart = true
controller!!.seekTo(position) // controller!!.seekTo(position)
updateUi(controller!!.getMedia()) // updateUi(controller!!.getMedia())
sbPosition.highlightCurrentChapter() // sbPosition.highlightCurrentChapter()
} // }
txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: ("" txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: (""
+ "\n" + Converter.getDurationStringLong(position)) + "\n" + Converter.getDurationStringLong(position))
} else { } else {
@ -538,7 +442,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
fun fadePlayerToToolbar(slideOffset: Float) { fun fadePlayerToToolbar(slideOffset: Float) {
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat() 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.alpha = 1 - playerFadeProgress
player.visibility = if (playerFadeProgress > 0.99f) View.INVISIBLE else View.VISIBLE 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() 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 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 { companion object {
const val TAG: String = "AudioPlayerFragment" 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) val feedPlaybackSpeedPreference = findPreference<Preference>(PREF_FEED_PLAYBACK_SPEED)
feedPlaybackSpeedPreference!!.onPreferenceClickListener = feedPlaybackSpeedPreference!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
val viewBinding = val viewBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater)
PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater)
viewBinding.seekBar.setProgressChangedListener { speed: Float? -> viewBinding.seekBar.setProgressChangedListener { speed: Float? ->
viewBinding.currentSpeedLabel.text = String.format( viewBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed)
Locale.getDefault(), "%.2fx", speed)
} }
viewBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> viewBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
viewBinding.seekBar.isEnabled = !isChecked viewBinding.seekBar.isEnabled = !isChecked
@ -224,8 +222,8 @@ class FeedSettingsFragment : Fragment() {
.setTitle(R.string.playback_speed) .setTitle(R.string.playback_speed)
.setView(viewBinding.root) .setView(viewBinding.root)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val newSpeed = if (viewBinding.useGlobalCheckbox.isChecked val newSpeed = if (viewBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
) FeedPreferences.SPEED_USE_GLOBAL else viewBinding.seekBar.currentSpeed else viewBinding.seekBar.currentSpeed
feedPreferences!!.feedPlaybackSpeed = newSpeed feedPreferences!!.feedPlaybackSpeed = newSpeed
if (feedPreferences != null) DBWriter.setFeedPreferences(feedPreferences!!) if (feedPreferences != null) DBWriter.setFeedPreferences(feedPreferences!!)
EventBus.getDefault().post( 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.preferences.UserPreferences
import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.*
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.storage.model.feed.SortOrder
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.QueueRecyclerAdapter import ac.mdiq.podcini.ui.adapter.QueueRecyclerAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter 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.Converter
import ac.mdiq.podcini.util.FeedItemUtil import ac.mdiq.podcini.util.FeedItemUtil
import ac.mdiq.podcini.util.event.* import ac.mdiq.podcini.util.event.*
import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
@ -311,6 +310,18 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
refreshToolbarState() 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) @Subscribe(threadMode = ThreadMode.MAIN)
fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) { fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) {
refreshSwipeTelltale() refreshSwipeTelltale()

View File

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

View File

@ -8,7 +8,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/playerFragment" android:id="@+id/playerFragment1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top" android:layout_gravity="top"
@ -35,7 +35,7 @@
android:id="@+id/itemDescription" android:id="@+id/itemDescription"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_above="@id/playtime_layout" android:layout_above="@id/playerFragment2"
android:layout_below="@id/toolbar" android:layout_below="@id/toolbar"
android:layout_marginBottom="12dp" /> android:layout_marginBottom="12dp" />
@ -75,231 +75,13 @@
tools:text="1:06:29" /> tools:text="1:06:29" />
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<LinearLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/playtime_layout" android:id="@+id/playerFragment2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layoutDirection="ltr" tools:layout_height="@dimen/external_player_height" />
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>
</RelativeLayout> </RelativeLayout>

View File

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

View File

@ -58,12 +58,33 @@
android:text="@string/All" /> android:text="@string/All" />
</RadioGroup> </RadioGroup>
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:orientation="horizontal">
android:text="@string/speed_presets"
style="@style/Podcini.TextView.ListItemPrimaryTitle" /> <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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/selected_speeds_grid" android:id="@+id/selected_speeds_grid"

View File

@ -27,6 +27,9 @@
<string name="years_statistics_label">Years</string> <string name="years_statistics_label">Years</string>
<string name="notification_pref_fragment">Notifications</string> <string name="notification_pref_fragment">Notifications</string>
<string name="current_playing_episode">Current</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" translatable="false">Podcini Echo</string>
<string name="podcini_echo_year" translatable="false">Podcini Echo %d</string> <string name="podcini_echo_year" translatable="false">Podcini Echo %d</string>

View File

@ -203,3 +203,9 @@
## 4.5.1 ## 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