4.8.0 commit

This commit is contained in:
Xilin Jia 2024-04-13 19:45:51 +00:00
parent 26115b436f
commit 1f8bb954a0
30 changed files with 1122 additions and 734 deletions

View File

@ -60,6 +60,10 @@ Even so, the database remains backward compatible, and AntennaPod's db can be ea
* enabled intro- and end- skipping
* mark as played when finished
* streamed media is added to queue and is resumed after restart
* new video episode view, with video player on top and episode descriptions in portrait mode
* easy switches on video player to other video mode or audio only
* default video player mode setting in preferences
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
### Podcast/Episode list

View File

@ -149,8 +149,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020129
versionName "4.7.1"
versionCode 3020130
versionName "4.8.0"
def commit = ""
try {
@ -238,6 +238,7 @@ dependencies {
implementation "androidx.work:work-runtime:2.9.0"
implementation "androidx.core:core-splashscreen:1.0.1"
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.webkit:webkit:1.9.0'
implementation "com.google.android.material:material:1.11.0"

View File

@ -246,11 +246,12 @@
android:value="ac.mdiq.podcini.ui.activity.PreferenceActivity"/>
</activity>
<!-- android:screenOrientation="sensorLandscape"-->
<activity
android:name=".ui.activity.VideoplayerActivity"
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
android:supportsPictureInPicture="true"
android:screenOrientation="sensorLandscape"
android:exported="false">
<meta-data
android:name="android.support.PARENT_ACTIVITY"

View File

@ -267,6 +267,14 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
}
}
fun ensureService() {
if (media == null) return
if (playbackService == null) {
PlaybackServiceStarter(activity, media!!).start()
Log.w(TAG, "playbackservice was null, restarted!")
}
}
fun playPause() {
if (media == null) return
if (playbackService == null) {

View File

@ -1626,6 +1626,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
} else {
if (codeArray != null && codeArray.size == 3) {
Log.d(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
if (codeArray[2]) setPlaybackSpeed(speed)
if (codeArray[1]) {
var item = (playable as? FeedMedia)?.item ?: currentitem

View File

@ -62,28 +62,21 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
val iconSize = (128 * context.resources.displayMetrics.density).toInt()
val options = RequestOptions().centerCrop()
try {
var imgLoc = playable?.getImageLocation()
when {
!imgLoc.isNullOrBlank() -> {
val imgLoc = playable?.getImageLocation()
val imgLoc1 = ImageResourceUtils.getFallbackImageLocation(playable!!)
Log.d(TAG, "loadIcon imgLoc $imgLoc $imgLoc1")
cachedIcon = Glide.with(context)
.asBitmap()
.load(imgLoc)
.apply(options)
.submit(iconSize, iconSize)
.get()
}
playable != null -> {
imgLoc = ImageResourceUtils.getFallbackImageLocation(playable!!)
if (!imgLoc.isNullOrBlank()) {
cachedIcon = Glide.with(context)
.error(Glide.with(context)
.asBitmap()
.load(imgLoc)
.load(imgLoc1)
.apply(options)
.submit(iconSize, iconSize)
.get())
.apply(options)
.submit(iconSize, iconSize)
.get()
}
}
}
} catch (ignore: InterruptedException) {
Log.e(TAG, "Media icon loader was interrupted")
} catch (tr: Throwable) {

View File

@ -111,6 +111,7 @@ object UserPreferences {
private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"
private const val PREF_REWIND_SECS = "prefRewindSecs"
private const val PREF_QUEUE_LOCKED = "prefQueueLocked"
private const val PREF_VIDEO_MODE = "prefVideoPlaybackMode"
// Experimental
const val EPISODE_CLEANUP_QUEUE: Int = -1
@ -410,6 +411,17 @@ object UserPreferences {
}
}
val videoPlayMode: Int
get() {
try {
return prefs.getString(PREF_VIDEO_MODE, "1")!!.toInt()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setVideoMode(1)
return 1
}
}
@JvmStatic
var videoPlaybackSpeed: Float
get() {
@ -661,6 +673,13 @@ object UserPreferences {
.apply()
}
@JvmStatic
fun setVideoMode(mode: Int) {
prefs.edit()
.putString(PREF_VIDEO_MODE, mode.toString())
.apply()
}
@JvmStatic
fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
prefs.edit()

View File

@ -5,10 +5,7 @@ import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UsageStatistics.doNotAskAgain
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.dialog.EditFallbackSpeedDialog
import ac.mdiq.podcini.ui.dialog.EditForwardSpeedDialog
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import android.app.Activity
import android.os.Build
@ -47,6 +44,12 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true
}
findPreference<Preference>(PREF_PLAYBACK_VIDEO_MODE_LAUNCHER)?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
VideoModeDialog.showDialog(requireContext())
true
}
findPreference<Preference>(PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER)!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
EditForwardSpeedDialog(requireActivity()).show()
@ -138,5 +141,6 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
private const val PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER = "prefPlaybackSpeedForwardLauncher"
private const val PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"
private const val PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload"
private const val PREF_PLAYBACK_VIDEO_MODE_LAUNCHER = "prefPlaybackVideoModeLauncher"
}
}

View File

@ -286,6 +286,9 @@ class MainActivity : CastEnabledActivity() {
}
override fun onSlide(view: View, slideOffset: Float) {
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
// audioPlayer.scrollToTop()
// }
audioPlayer.fadePlayerToToolbar(slideOffset)
}
}

View File

@ -11,7 +11,7 @@ class PlaybackSpeedDialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getTranslucentTheme(this))
super.onCreate(savedInstanceState)
val speedDialog: VariableSpeedDialog? = VariableSpeedDialog.newInstance(booleanArrayOf(false, false, true), 2)
val speedDialog: VariableSpeedDialog? = VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), 2)
speedDialog?.show(supportFragmentManager, null)
}

View File

@ -113,15 +113,13 @@ class SelectSubscriptionActivity : AppCompatActivity() {
.apply(RequestOptions.overrideOf(iconSize, iconSize))
.listener(object : RequestListener<Bitmap?> {
@UnstableApi override fun onLoadFailed(e: GlideException?, model: Any?,
target: Target<Bitmap?>, isFirstResource: Boolean
): Boolean {
target: Target<Bitmap?>, isFirstResource: Boolean): Boolean {
addShortcut(feed, null)
return true
}
@UnstableApi override fun onResourceReady(resource: Bitmap, model: Any,
target: Target<Bitmap?>, dataSource: DataSource, isFirstResource: Boolean
): Boolean {
target: Target<Bitmap?>, dataSource: DataSource, isFirstResource: Boolean): Boolean {
addShortcut(feed, resource)
return true
}

View File

@ -1,107 +1,106 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.util.Converter.getDurationStringLong
import ac.mdiq.podcini.util.FeedItemUtil.getLinkWithFallback
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
import ac.mdiq.podcini.util.TimeSpeedConverter
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent
import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter
import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.util.FeedItemUtil.getLinkWithFallback
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
import ac.mdiq.podcini.util.event.MessageEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent
import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent
import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.PixelFormat
import android.graphics.drawable.ColorDrawable
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.*
import android.view.View.OnTouchListener
import android.view.animation.*
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.Observable
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
/**
* Activity for playing video files.
*/
@UnstableApi
class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
class VideoplayerActivity : CastEnabledActivity() {
private var _binding: VideoplayerActivityBinding? = null
private val binding get() = _binding!!
/**
* True if video controls are currently visible.
*/
private var videoControlsShowing = true
private var videoSurfaceCreated = false
private var destroyingDueToReload = false
private var lastScreenTap: Long = 0
private val videoControlsHider = Handler(Looper.getMainLooper())
private var controller: PlaybackController? = null
private var showTimeLeft = false
private var isFavorite = false
private var switchToAudioOnly = false
private var disposable: Disposable? = null
private var prog = 0f
lateinit var videoEpisodeFragment: VideoEpisodeFragment
var videoMode = 0
var switchToAudioOnly = false
override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
videoMode = intent.getIntExtra("fullScreenMode",0)
if (videoMode == 0) {
videoMode = videoPlayMode
if (videoMode == AUDIO_ONLY) {
switchToAudioOnly = true
finish()
}
if (videoMode != FULL_SCREEN_VIEW && videoMode != WINDOW_VIEW) {
Log.i(TAG, "videoMode not selected, use window mode")
videoMode = WINDOW_VIEW
}
}
when (videoMode) {
FULL_SCREEN_VIEW -> {
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
// has to be called before setting layout content
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
setTheme(R.style.Theme_Podcini_VideoPlayer)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.setFormat(PixelFormat.TRANSPARENT)
}
WINDOW_VIEW -> {
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
setTheme(R.style.Theme_Podcini_VideoEpisode)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
window.setFormat(PixelFormat.TRANSPARENT)
}
}
super.onCreate(savedInstanceState)
window.setFormat(PixelFormat.TRANSPARENT)
_binding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
setupView()
supportActionBar?.setBackgroundDrawable(ColorDrawable(-0x80000000))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
controller = newPlaybackController()
controller!!.init()
loadMediaInfo()
val fm = supportFragmentManager
val transaction = fm.beginTransaction()
videoEpisodeFragment = VideoEpisodeFragment()
transaction.replace(R.id.main_view, videoEpisodeFragment, VideoEpisodeFragment.TAG)
transaction.commit()
}
@UnstableApi
@ -111,7 +110,7 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
if (isCasting) {
val intent = getPlayerActivityIntent(this)
if (intent.component?.className != VideoplayerActivity::class.java.name) {
destroyingDueToReload = true
videoEpisodeFragment.destroyingDueToReload = true
finish()
startActivity(intent)
}
@ -120,21 +119,17 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
override fun onDestroy() {
super.onDestroy()
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
_binding = null
controller?.release()
controller = null // prevent leak
disposable?.dispose()
}
@UnstableApi
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
videoControlsHider.removeCallbacks(hideVideoControls)
}
// Controller released; we will not receive buffering updates
binding.progressBar.visibility = View.GONE
}
public override fun onUserLeaveHint() {
@ -146,20 +141,9 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
@UnstableApi
override fun onStart() {
super.onStart()
onPositionObserverUpdate()
EventBus.getDefault().register(this)
}
@UnstableApi
override fun onPause() {
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
if (controller?.status == PlayerStatus.PLAYING) {
controller!!.pause()
}
}
super.onPause()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.get(this).trimMemory(level)
@ -170,47 +154,11 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
Glide.get(this).clearMemory()
}
@UnstableApi
private fun newPlaybackController(): PlaybackController {
return object : PlaybackController(this@VideoplayerActivity) {
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
binding.playButton.setIsShowPlay(showPlay)
if (showPlay) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setupVideoAspectRatio()
if (videoSurfaceCreated && controller != null) {
Log.d(TAG, "Videosurface already created, setting videosurface now")
controller!!.setVideoSurface(binding.videoView.holder)
}
}
}
override fun loadMediaInfo() {
this@VideoplayerActivity.loadMediaInfo()
}
override fun onPlaybackEnd() {
fun toggleViews() {
val newIntent = Intent(this, VideoplayerActivity::class.java)
newIntent.putExtra("fullScreenMode", if (videoMode == FULL_SCREEN_VIEW) WINDOW_VIEW else FULL_SCREEN_VIEW)
finish()
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused")
fun bufferUpdate(event: BufferUpdateEvent) {
when {
event.hasStarted() -> {
binding.progressBar.visibility = View.VISIBLE
}
event.hasEnded() -> {
binding.progressBar.visibility = View.INVISIBLE
}
else -> {
binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
}
}
startActivity(newIntent)
}
@Subscribe(threadMode = ThreadMode.MAIN)
@ -221,266 +169,6 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
}
}
@UnstableApi
private fun loadMediaInfo() {
Log.d(TAG, "loadMediaInfo()")
if (controller?.getMedia() == null) return
if (controller!!.status == PlayerStatus.PLAYING && !controller!!.isPlayingVideoLocally) {
Log.d(TAG, "Closing, no longer video")
destroyingDueToReload = true
finish()
MainActivityStarter(this).withOpenPlayer().start()
return
}
showTimeLeft = shouldShowRemainingTime()
onPositionObserverUpdate()
checkFavorite()
val media = controller!!.getMedia()
if (media != null) {
supportActionBar!!.subtitle = media.getEpisodeTitle()
supportActionBar!!.title = media.getFeedTitle()
}
}
@UnstableApi
private fun setupView() {
showTimeLeft = shouldShowRemainingTime()
Log.d("timeleft", if (showTimeLeft) "true" else "false")
binding.durationLabel.setOnClickListener {
showTimeLeft = !showTimeLeft
val media = controller?.getMedia() ?: return@setOnClickListener
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val length: String
if (showTimeLeft) {
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
length = "-" + getDurationStringLong(remainingTime)
} else {
val duration = converter.convert(media.getDuration())
length = getDurationStringLong(duration)
}
binding.durationLabel.text = length
setShowRemainTimeSetting(showTimeLeft)
Log.d("timeleft on click", if (showTimeLeft) "true" else "false")
}
binding.sbPosition.setOnSeekBarChangeListener(this)
binding.rewindButton.setOnClickListener { onRewind() }
binding.rewindButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity,
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true
}
binding.playButton.setIsVideoScreen(true)
binding.playButton.setOnClickListener { onPlayPause() }
binding.fastForwardButton.setOnClickListener { onFastForward() }
binding.fastForwardButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity,
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
false
}
// To suppress touches directly below the slider
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
binding.bottomControlsContainer.fitsSystemWindows = true
binding.videoView.holder.addCallback(surfaceHolderCallback)
binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
setupVideoControlsToggler()
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
binding.videoView.setAvailableSize(
binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
}
}
private val hideVideoControls = Runnable {
if (videoControlsShowing) {
Log.d(TAG, "Hiding video controls")
supportActionBar?.hide()
hideVideoControls(true)
videoControlsShowing = false
}
}
private val onVideoviewTouched = OnTouchListener { v: View, event: MotionEvent ->
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
if (PictureInPictureUtil.isInPictureInPictureMode(this)) return@OnTouchListener true
videoControlsHider.removeCallbacks(hideVideoControls)
if (System.currentTimeMillis() - lastScreenTap < 300) {
if (event.x > v.measuredWidth / 2.0f) {
onFastForward()
showSkipAnimation(true)
} else {
onRewind()
showSkipAnimation(false)
}
if (videoControlsShowing) {
supportActionBar?.hide()
hideVideoControls(false)
videoControlsShowing = false
}
return@OnTouchListener true
}
toggleVideoControlsVisibility()
if (videoControlsShowing) setupVideoControlsToggler()
lastScreenTap = System.currentTimeMillis()
true
}
private fun showSkipAnimation(isForward: Boolean) {
val skipAnimation = AnimationSet(true)
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
skipAnimation.fillAfter = false
skipAnimation.duration = 800
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
if (isForward) {
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
} else {
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
}
binding.skipAnimationImage.visibility = View.VISIBLE
binding.skipAnimationImage.layoutParams = params
binding.skipAnimationImage.startAnimation(skipAnimation)
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
}
override fun onAnimationEnd(animation: Animation) {
binding.skipAnimationImage.visibility = View.GONE
}
override fun onAnimationRepeat(animation: Animation) {
}
})
}
private fun setupVideoControlsToggler() {
videoControlsHider.removeCallbacks(hideVideoControls)
videoControlsHider.postDelayed(hideVideoControls, 2500)
}
@UnstableApi
private fun setupVideoAspectRatio() {
if (videoSurfaceCreated && controller != null) {
val videoSize = controller!!.videoSize
if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) {
Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second)
binding.videoView.setVideoSize(videoSize.first, videoSize.second)
} else {
Log.e(TAG, "Could not determine video size")
}
}
}
private fun toggleVideoControlsVisibility() {
if (videoControlsShowing) {
supportActionBar?.hide()
hideVideoControls(true)
} else {
supportActionBar?.show()
showVideoControls()
}
videoControlsShowing = !videoControlsShowing
}
@UnstableApi
fun onRewind() {
if (controller == null) return
val curr = controller!!.position
controller!!.seekTo(curr - rewindSecs * 1000)
setupVideoControlsToggler()
}
@UnstableApi
fun onPlayPause() {
if (controller == null) return
controller!!.playPause()
setupVideoControlsToggler()
}
@UnstableApi
fun onFastForward() {
if (controller == null) return
val curr = controller!!.position
controller!!.seekTo(curr + fastForwardSecs * 1000)
setupVideoControlsToggler()
}
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
holder.setFixedSize(width, height)
}
@UnstableApi
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d(TAG, "Videoview holder created")
videoSurfaceCreated = true
if (controller?.status == PlayerStatus.PLAYING) {
controller!!.setVideoSurface(holder)
}
setupVideoAspectRatio()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d(TAG, "Videosurface was destroyed")
videoSurfaceCreated = false
if (controller != null && !destroyingDueToReload && !switchToAudioOnly) {
controller!!.notifyVideoSurfaceAbandoned()
}
}
}
private fun showVideoControls() {
binding.bottomControlsContainer.visibility = View.VISIBLE
binding.controlsContainer.visibility = View.VISIBLE
val animation = AnimationUtils.loadAnimation(this, R.anim.fade_in)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
private fun hideVideoControls(showAnimation: Boolean) {
if (showAnimation) {
val animation = AnimationUtils.loadAnimation(this, R.anim.fade_out)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
}
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
binding.bottomControlsContainer.fitsSystemWindows = true
binding.bottomControlsContainer.visibility = View.GONE
binding.controlsContainer.visibility = View.GONE
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: PlaybackPositionEvent?) {
onPositionObserverUpdate()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
@ -516,9 +204,9 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
@UnstableApi
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
super.onPrepareOptionsMenu(menu)
if (controller == null) return false
val controller = videoEpisodeFragment.controller ?: return false
val media = controller!!.getMedia()
val media = controller.getMedia()
val isFeedMedia = (media is FeedMedia)
menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia) // FeedMedia implies it belongs to a Feed
@ -533,22 +221,33 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
menu.findItem(R.id.add_to_favorites_item).setVisible(false)
menu.findItem(R.id.remove_from_favorites_item).setVisible(false)
if (isFeedMedia) {
menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite)
menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite)
menu.findItem(R.id.add_to_favorites_item).setVisible(!videoEpisodeFragment.isFavorite)
menu.findItem(R.id.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite)
}
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller!!.sleepTimerActive())
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller!!.sleepTimerActive())
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive())
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive())
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
menu.findItem(R.id.audio_controls).setVisible(controller!!.audioTracks.size >= 2)
menu.findItem(R.id.audio_controls).setVisible(controller.audioTracks.size >= 2)
menu.findItem(R.id.playback_speed).setVisible(true)
menu.findItem(R.id.player_show_chapters).setVisible(true)
if (videoMode == WINDOW_VIEW) {
menu.findItem(R.id.add_to_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
menu.findItem(R.id.remove_from_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
menu.findItem(R.id.set_sleeptimer_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
menu.findItem(R.id.disable_sleeptimer_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
menu.findItem(R.id.player_switch_to_audio_only).setShowAsAction(SHOW_AS_ACTION_NEVER)
menu.findItem(R.id.open_feed_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
menu.findItem(R.id.share_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val controller = videoEpisodeFragment.controller
// some options option requires FeedItem
when {
item.itemId == R.id.player_switch_to_audio_only -> {
@ -571,17 +270,17 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
return false
}
else -> {
val media = controller?.getMedia() ?: return false
val media = controller.getMedia() ?: return false
val feedItem = getFeedItem(media) // some options option requires FeedItem
when {
item.itemId == R.id.add_to_favorites_item && feedItem != null -> {
DBWriter.addFavoriteItem(feedItem)
isFavorite = true
videoEpisodeFragment.isFavorite = true
invalidateOptionsMenu()
}
item.itemId == R.id.remove_from_favorites_item && feedItem != null -> {
DBWriter.removeFavoriteItem(feedItem)
isFavorite = false
videoEpisodeFragment.isFavorite = false
invalidateOptionsMenu()
}
item.itemId == R.id.disable_sleeptimer_item || item.itemId == R.id.set_sleeptimer_item -> {
@ -615,93 +314,10 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
}
}
fun onPositionObserverUpdate() {
if (controller == null) return
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val currentPosition = converter.convert(controller!!.position)
val duration = converter.convert(controller!!.duration)
val remainingTime = converter.convert(
controller!!.duration - controller!!.position)
// 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")
return
}
binding.positionLabel.text = getDurationStringLong(currentPosition)
if (showTimeLeft) {
binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
} else {
binding.durationLabel.text = getDurationStringLong(duration)
}
updateProgressbarPosition(currentPosition, duration)
}
private fun updateProgressbarPosition(position: Int, duration: Int) {
Log.d(TAG, "updateProgressbarPosition($position, $duration)")
val progress = (position.toFloat()) / duration
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (controller == null) return
if (fromUser) {
prog = progress / (seekBar.max.toFloat())
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val position = converter.convert((prog * controller!!.duration).toInt())
binding.seekPositionLabel.text = getDurationStringLong(position)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
binding.seekCardView.scaleX = .8f
binding.seekCardView.scaleY = .8f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start()
videoControlsHider.removeCallbacks(hideVideoControls)
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
if (controller != null) {
controller!!.seekTo((prog * controller!!.duration).toInt())
}
binding.seekCardView.scaleX = 1f
binding.seekCardView.scaleY = 1f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start()
setupVideoControlsToggler()
}
private fun checkFavorite() {
val feedItem = getFeedItem(controller?.getMedia()) ?: return
disposable?.dispose()
disposable = Observable.fromCallable { DBReader.getFeedItem(feedItem.id) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ item: FeedItem? ->
if (item != null) {
val isFav = item.isTagged(FeedItem.TAG_FAVORITE)
if (isFavorite != isFav) {
isFavorite = isFav
invalidateOptionsMenu()
}
}
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
}
private fun compatEnterPictureInPicture() {
if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
supportActionBar?.hide()
hideVideoControls(false)
if (videoMode == FULL_SCREEN_VIEW) supportActionBar?.hide()
videoEpisodeFragment.hideVideoControls(false)
enterPictureInPictureMode()
}
}
@ -715,18 +331,18 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
when (keyCode) {
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE -> {
onPlayPause()
toggleVideoControlsVisibility()
videoEpisodeFragment.onPlayPause()
videoEpisodeFragment.toggleVideoControlsVisibility()
return true
}
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> {
onRewind()
showSkipAnimation(false)
videoEpisodeFragment.onRewind()
videoEpisodeFragment.showSkipAnimation(false)
return true
}
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> {
onFastForward()
showSkipAnimation(true)
videoEpisodeFragment.onFastForward()
videoEpisodeFragment.showSkipAnimation(true)
return true
}
KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_ESCAPE -> {
@ -756,7 +372,8 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
}
//Go to x% of video:
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
controller?.seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller!!.duration).toInt())
val controller = videoEpisodeFragment.controller
controller?.seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller.duration).toInt())
return true
}
return super.onKeyUp(keyCode, event)
@ -764,23 +381,26 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
companion object {
private const val TAG = "VideoplayerActivity"
const val WINDOW_VIEW = 1
const val FULL_SCREEN_VIEW = 2
const val AUDIO_ONLY = 3
private fun getWebsiteLinkWithFallback(media: Playable?): String? {
when {
return when {
media == null -> {
return null
null
}
!media.getWebsiteLink().isNullOrBlank() -> {
return media.getWebsiteLink()
media.getWebsiteLink()
}
media is FeedMedia -> {
return getLinkWithFallback(media.item)
getLinkWithFallback(media.item)
}
else -> return null
else -> null
}
}
private fun getFeedItem(playable: Playable?): FeedItem? {
fun getFeedItem(playable: Playable?): FeedItem? {
return if (playable is FeedMedia) {
playable.item
} else {

View File

@ -104,9 +104,6 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
binding.global.isChecked = true
}
}
// if (!settingCode[0]) binding.currentAudio.visibility = View.INVISIBLE
// if (!settingCode[1]) binding.currentPodcast.visibility = View.INVISIBLE
// if (!settingCode[2]) binding.global.visibility = View.INVISIBLE
speedSeekBar = binding.speedSeekBar
speedSeekBar.setProgressChangedListener { multiplier: Float ->
@ -184,9 +181,11 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
true
}
holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({
if (binding.currentAudio.isChecked) settingCode[0] = true
if (binding.currentPodcast.isChecked) settingCode[1] = true
if (binding.global.isChecked) settingCode[2] = true
Log.d("VariableSpeedDialog", "holder.chip settingCode0: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
settingCode[0] = binding.currentAudio.isChecked
settingCode[1] = binding.currentPodcast.isChecked
settingCode[2] = binding.global.isChecked
Log.d("VariableSpeedDialog", "holder.chip settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (controller != null) {
dismiss()
@ -203,13 +202,17 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
return selectedSpeeds[position].hashCode().toLong()
}
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(
chip)
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
}
companion object {
/**
* @param settingCode_ array at input indicate which categories can be set, at output which categories are changed
* @param index_default indicates which category is checked by default
*/
fun newInstance(settingCode_: BooleanArray? = null, index_default: Int? = null): VariableSpeedDialog? {
val settingCode = settingCode_ ?: BooleanArray(3){false}
val settingCode = settingCode_ ?: BooleanArray(3){true}
Log.d("VariableSpeedDialog", "newInstance settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (settingCode.size != 3) {
Log.e("VariableSpeedDialog", "wrong settingCode dimension")
return null

View File

@ -0,0 +1,33 @@
package ac.mdiq.podcini.ui.dialog
import android.content.Context
import android.content.DialogInterface
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ac.mdiq.podcini.R
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import ac.mdiq.podcini.preferences.UserPreferences.feedOrder
import ac.mdiq.podcini.preferences.UserPreferences.setFeedOrder
import ac.mdiq.podcini.preferences.UserPreferences.setVideoMode
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import org.greenrobot.eventbus.EventBus
object VideoModeDialog {
fun showDialog(context: Context) {
val dialog = MaterialAlertDialogBuilder(context)
dialog.setTitle(context.getString(R.string.pref_playback_video_mode))
dialog.setNegativeButton(android.R.string.cancel) { d: DialogInterface, _: Int -> d.dismiss() }
val selected = videoPlayMode
val entryValues = listOf(*context.resources.getStringArray(R.array.video_mode_options_values))
val selectedIndex = entryValues.indexOf("" + selected)
val items = context.resources.getStringArray(R.array.video_mode_options)
dialog.setSingleChoiceItems(items, selectedIndex) { d: DialogInterface, which: Int ->
if (selectedIndex != which) {
setVideoMode(entryValues[which].toInt())
}
d.dismiss()
}
dialog.show()
}
}

View File

@ -6,11 +6,14 @@ 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.PlaybackController.Companion
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.storage.model.feed.Chapter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
@ -23,6 +26,7 @@ import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.AUDIO_ONLY
import ac.mdiq.podcini.ui.view.ChapterSeekBar
import ac.mdiq.podcini.ui.view.PlayButton
import ac.mdiq.podcini.util.ChapterUtils
@ -97,7 +101,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private var currentMedia: Playable? = null
private var currentitem: FeedItem? = null
@SuppressLint("WrongConstant")
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
@ -447,6 +450,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
}
@JvmOverloads
fun scrollToTop() {
itemDescFrag.scrollToTop()
}
fun fadePlayerToToolbar(slideOffset: Float) {
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat()
val player = playerView1
@ -515,9 +523,12 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
Log.d(TAG, "internalPlayerFragment was clicked")
val media = controller?.getMedia()
if (media != null) {
if (media.getMediaType() == MediaType.AUDIO) {
if (media.getMediaType() == MediaType.AUDIO || videoPlayMode == AUDIO_ONLY) {
controller!!.ensureService()
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else {
controller?.playPause()
// controller!!.ensureService()
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media)
startActivity(intent)
}
@ -712,19 +723,17 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
.fitCenter()
.dontAnimate()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media) + "sdfsdf"
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
when {
!imgLoc.isNullOrBlank() -> Glide.with(this)
Glide.with(this)
.load(imgLoc)
.apply(options)
.into(imgvCover)
!imgLocFB.isNullOrBlank() -> Glide.with(this)
.error(Glide.with(this)
.load(imgLocFB)
.error(R.mipmap.ic_launcher)
.apply(options))
.apply(options)
.into(imgvCover)
else -> imgvCover.setImageResource(R.mipmap.ic_launcher)
}
if (controller?.isPlayingVideoLocally == true) {
(activity as MainActivity).bottomSheet.setLocked(true)
@ -747,7 +756,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
}
companion object {
const val TAG: String = "AudioPlayerFragment"
}

View File

@ -316,7 +316,6 @@ class PlayerDetailsFragment : Fragment() {
@UnstableApi private fun seekToPrevChapter() {
val curr: Chapter? = currentChapter
if (controller == null || curr == null || displayedChapterIndex == -1) return
when {
@ -353,8 +352,8 @@ class PlayerDetailsFragment : Fragment() {
val prefs = requireActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE)
val editor = prefs.edit()
if (controller?.getMedia() != null) {
Log.d(TAG, "Saving scroll position: " + webvDescription.scrollY)
editor.putInt(PREF_SCROLL_Y, webvDescription.scrollY)
Log.d(TAG, "Saving scroll position: " + binding.itemDescriptionFragment.scrollY)
editor.putInt(PREF_SCROLL_Y, binding.itemDescriptionFragment.scrollY)
editor.putString(PREF_PLAYABLE_ID, controller!!.getMedia()!!.getIdentifier().toString())
} else {
Log.d(TAG, "savePreferences was called while media or webview was null")
@ -374,11 +373,12 @@ class PlayerDetailsFragment : Fragment() {
if (scrollY != -1) {
if (id == controller?.getMedia()?.getIdentifier()?.toString()) {
Log.d(TAG, "Restored scroll Position: $scrollY")
webvDescription.scrollTo(webvDescription.scrollX, scrollY)
binding.itemDescriptionFragment.scrollTo(binding.itemDescriptionFragment.scrollX, scrollY)
return true
}
Log.d(TAG, "reset scroll Position: 0")
webvDescription.scrollTo(webvDescription.scrollX, 0)
binding.itemDescriptionFragment.scrollTo(0, 0)
return true
}
}
@ -386,7 +386,7 @@ class PlayerDetailsFragment : Fragment() {
}
fun scrollToTop() {
webvDescription.scrollTo(0, 0)
binding.itemDescriptionFragment.scrollTo(0, 0)
savePreference()
}

View File

@ -0,0 +1,571 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.activity.VideoplayerActivity
import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.Converter.getDurationStringLong
import ac.mdiq.podcini.util.TimeSpeedConverter
import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.*
import android.view.animation.*
import android.widget.FrameLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.media3.common.util.UnstableApi
import io.reactivex.Observable
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
@UnstableApi
class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
private var _binding: VideoEpisodeFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var root: ViewGroup
/**
* True if video controls are currently visible.
*/
private var videoControlsShowing = true
private var videoSurfaceCreated = false
private var lastScreenTap: Long = 0
private val videoControlsHider = Handler(Looper.getMainLooper())
private var showTimeLeft = false
private var disposable: Disposable? = null
private var prog = 0f
private var itemsLoaded = false
private var item: FeedItem? = null
private var webviewData: String? = null
private lateinit var webvDescription: ShownotesWebView
var destroyingDueToReload = false
var controller: PlaybackController? = null
var isFavorite = false
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
root = binding.root
controller = newPlaybackController()
controller!!.init()
// loadMediaInfo()
setupView()
return root
}
@OptIn(UnstableApi::class) private fun newPlaybackController(): PlaybackController {
return object : PlaybackController(requireActivity()) {
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
Log.d(TAG, "updatePlayButtonShowsPlay called")
binding.playButton.setIsShowPlay(showPlay)
if (showPlay) {
(activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setupVideoAspectRatio()
if (videoSurfaceCreated && controller != null) {
Log.d(TAG, "Videosurface already created, setting videosurface now")
controller!!.setVideoSurface(binding.videoView.holder)
}
}
}
override fun loadMediaInfo() {
this@VideoEpisodeFragment.loadMediaInfo()
}
override fun onPlaybackEnd() {
activity?.finish()
}
}
}
@UnstableApi
override fun onStart() {
super.onStart()
onPositionObserverUpdate()
EventBus.getDefault().register(this)
}
@UnstableApi
override fun onPause() {
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
if (controller?.status == PlayerStatus.PLAYING) {
controller!!.pause()
}
}
super.onPause()
}
@UnstableApi
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
videoControlsHider.removeCallbacks(hideVideoControls)
}
// Controller released; we will not receive buffering updates
binding.progressBar.visibility = View.GONE
}
override fun onDestroyView() {
super.onDestroyView()
root.removeView(webvDescription)
webvDescription.destroy()
_binding = null
controller?.release()
controller = null // prevent leak
disposable?.dispose()
}
@Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused")
fun bufferUpdate(event: BufferUpdateEvent) {
when {
event.hasStarted() -> {
binding.progressBar.visibility = View.VISIBLE
}
event.hasEnded() -> {
binding.progressBar.visibility = View.INVISIBLE
}
else -> {
binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
}
}
}
private fun setupVideoAspectRatio() {
if (videoSurfaceCreated && controller != null) {
val videoSize = controller!!.videoSize
if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) {
Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second)
val videoWidth = resources.displayMetrics.widthPixels
val videoHeight = (videoWidth.toFloat() / videoSize.first * videoSize.second).toInt()
Log.d(TAG, "Width,height of video: " + videoWidth + ", " + videoHeight)
binding.videoView.setVideoSize(videoWidth, videoHeight)
// binding.videoView.setVideoSize(videoSize.first, videoSize.second)
// binding.videoView.setVideoSize(-1, -1)
} else {
Log.e(TAG, "Could not determine video size")
val videoWidth = resources.displayMetrics.widthPixels
val videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
Log.d(TAG, "Width,height of video: " + videoWidth + ", " + videoHeight)
binding.videoView.setVideoSize(videoWidth, videoHeight)
}
}
}
@OptIn(UnstableApi::class) private fun loadMediaInfo() {
Log.d(TAG, "loadMediaInfo called")
if (controller?.getMedia() == null) return
if (controller!!.status == PlayerStatus.PLAYING && !controller!!.isPlayingVideoLocally) {
Log.d(TAG, "Closing, no longer video")
destroyingDueToReload = true
activity?.finish()
MainActivityStarter(requireContext()).withOpenPlayer().start()
return
}
showTimeLeft = shouldShowRemainingTime()
onPositionObserverUpdate()
load()
val media = controller!!.getMedia()
if (media != null) {
(activity as AppCompatActivity).supportActionBar!!.subtitle = media.getEpisodeTitle()
(activity as AppCompatActivity).supportActionBar!!.title = media.getFeedTitle()
}
}
@UnstableApi private fun load() {
disposable?.dispose()
Log.d(TAG, "load() called")
disposable = Observable.fromCallable<FeedItem?> { this.loadInBackground() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result: FeedItem? ->
item = result
Log.d(TAG, "load() item ${item?.id}")
if (item != null) {
val isFav = item!!.isTagged(FeedItem.TAG_FAVORITE)
if (isFavorite != isFav) {
isFavorite = isFav
invalidateOptionsMenu(requireActivity())
}
}
onFragmentLoaded()
itemsLoaded = true
}, { error: Throwable? ->
Log.e(TAG, Log.getStackTraceString(error))
})
}
private fun loadInBackground(): FeedItem? {
val feedItem = VideoplayerActivity.getFeedItem(controller?.getMedia())
if (feedItem != null) {
val duration = feedItem.media?.getDuration()?: Int.MAX_VALUE
DBReader.loadDescriptionOfFeedItem(feedItem)
webviewData = ShownotesCleaner(requireContext(), feedItem.description?:"", duration).processShownotes()
}
return feedItem
}
@UnstableApi private fun onFragmentLoaded() {
if (webviewData != null && !itemsLoaded) {
webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData!!, "text/html", "utf-8", "about:blank")
}
}
@UnstableApi
private fun setupView() {
showTimeLeft = shouldShowRemainingTime()
Log.d(TAG, "setupView showTimeLeft: $showTimeLeft")
binding.durationLabel.setOnClickListener {
showTimeLeft = !showTimeLeft
val media = controller?.getMedia() ?: return@setOnClickListener
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val length: String
if (showTimeLeft) {
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
length = "-" + getDurationStringLong(remainingTime)
} else {
val duration = converter.convert(media.getDuration())
length = getDurationStringLong(duration)
}
binding.durationLabel.text = length
setShowRemainTimeSetting(showTimeLeft)
Log.d("timeleft on click", if (showTimeLeft) "true" else "false")
}
binding.sbPosition.setOnSeekBarChangeListener(this)
binding.rewindButton.setOnClickListener { onRewind() }
binding.rewindButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true
}
binding.playButton.setIsVideoScreen(true)
binding.playButton.setOnClickListener { onPlayPause() }
binding.fastForwardButton.setOnClickListener { onFastForward() }
binding.fastForwardButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
false
}
// To suppress touches directly below the slider
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
binding.videoView.holder.addCallback(surfaceHolderCallback)
binding.bottomControlsContainer.fitsSystemWindows = true
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
setupVideoControlsToggler()
// (activity as AppCompatActivity).window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
binding.videoView.setAvailableSize(
binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
}
webvDescription = binding.webvDescription
// webvDescription.setTimecodeSelectedListener { time: Int? ->
// val cMedia = controller?.getMedia()
// if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
// controller?.seekTo(time ?: 0)
// } else {
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
// Snackbar.LENGTH_LONG)
// }
// }
// registerForContextMenu(webvDescription)
// webvDescription.visibility = View.GONE
binding.toggleViews.setOnClickListener {
(activity as? VideoplayerActivity)?.toggleViews()
}
binding.audioOnly.setOnClickListener {
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
(activity as? VideoplayerActivity)?.finish()
}
}
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
videoControlsHider.removeCallbacks(hideVideoControls)
if (System.currentTimeMillis() - lastScreenTap < 300) {
if (event.x > v.measuredWidth / 2.0f) {
onFastForward()
showSkipAnimation(true)
} else {
onRewind()
showSkipAnimation(false)
}
if (videoControlsShowing) {
hideVideoControls(false)
if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW)
(activity as AppCompatActivity).supportActionBar?.hide()
videoControlsShowing = false
}
return@OnTouchListener true
}
toggleVideoControlsVisibility()
if (videoControlsShowing) setupVideoControlsToggler()
lastScreenTap = System.currentTimeMillis()
true
}
fun toggleVideoControlsVisibility() {
if (videoControlsShowing) {
hideVideoControls(true)
if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW)
(activity as AppCompatActivity).supportActionBar?.hide()
} else {
showVideoControls()
(activity as AppCompatActivity).supportActionBar?.show()
}
videoControlsShowing = !videoControlsShowing
}
fun showSkipAnimation(isForward: Boolean) {
val skipAnimation = AnimationSet(true)
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
skipAnimation.fillAfter = false
skipAnimation.duration = 800
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
if (isForward) {
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
} else {
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
}
binding.skipAnimationImage.visibility = View.VISIBLE
binding.skipAnimationImage.layoutParams = params
binding.skipAnimationImage.startAnimation(skipAnimation)
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
}
override fun onAnimationEnd(animation: Animation) {
binding.skipAnimationImage.visibility = View.GONE
}
override fun onAnimationRepeat(animation: Animation) {
}
})
}
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
holder.setFixedSize(width, height)
}
@UnstableApi
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d(TAG, "Videoview holder created")
videoSurfaceCreated = true
if (controller?.status == PlayerStatus.PLAYING) {
controller!!.setVideoSurface(holder)
}
setupVideoAspectRatio()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d(TAG, "Videosurface was destroyed")
videoSurfaceCreated = false
if (controller != null && !destroyingDueToReload && !(activity as VideoplayerActivity).switchToAudioOnly) {
controller!!.notifyVideoSurfaceAbandoned()
}
}
}
@UnstableApi
fun onRewind() {
if (controller == null) return
val curr = controller!!.position
controller!!.seekTo(curr - rewindSecs * 1000)
setupVideoControlsToggler()
}
@UnstableApi
fun onPlayPause() {
if (controller == null) return
controller!!.playPause()
setupVideoControlsToggler()
}
@UnstableApi
fun onFastForward() {
if (controller == null) return
val curr = controller!!.position
controller!!.seekTo(curr + fastForwardSecs * 1000)
setupVideoControlsToggler()
}
private fun setupVideoControlsToggler() {
videoControlsHider.removeCallbacks(hideVideoControls)
videoControlsHider.postDelayed(hideVideoControls, 2500)
}
private val hideVideoControls = Runnable {
if (videoControlsShowing) {
Log.d(TAG, "Hiding video controls")
hideVideoControls(true)
if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW)
(activity as? AppCompatActivity)?.supportActionBar?.hide()
videoControlsShowing = false
}
}
private fun showVideoControls() {
binding.bottomControlsContainer.visibility = View.VISIBLE
binding.controlsContainer.visibility = View.VISIBLE
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_in)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
binding.bottomControlsContainer.fitsSystemWindows = true
}
fun hideVideoControls(showAnimation: Boolean) {
if (showAnimation) {
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_out)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
}
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
// (activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
// or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
binding.bottomControlsContainer.fitsSystemWindows = true
binding.bottomControlsContainer.visibility = View.GONE
binding.controlsContainer.visibility = View.GONE
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: PlaybackPositionEvent?) {
onPositionObserverUpdate()
}
fun onPositionObserverUpdate() {
if (controller == null) return
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val currentPosition = converter.convert(controller!!.position)
val duration = converter.convert(controller!!.duration)
val remainingTime = converter.convert(controller!!.duration - controller!!.position)
// 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")
return
}
binding.positionLabel.text = getDurationStringLong(currentPosition)
if (showTimeLeft) {
binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
} else {
binding.durationLabel.text = getDurationStringLong(duration)
}
updateProgressbarPosition(currentPosition, duration)
}
private fun updateProgressbarPosition(position: Int, duration: Int) {
Log.d(TAG, "updateProgressbarPosition($position, $duration)")
val progress = (position.toFloat()) / duration
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (controller == null) return
if (fromUser) {
prog = progress / (seekBar.max.toFloat())
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
val position = converter.convert((prog * controller!!.duration).toInt())
binding.seekPositionLabel.text = getDurationStringLong(position)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
binding.seekCardView.scaleX = .8f
binding.seekCardView.scaleY = .8f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start()
videoControlsHider.removeCallbacks(hideVideoControls)
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
controller?.seekTo((prog * controller!!.duration).toInt())
binding.seekCardView.scaleX = 1f
binding.seekCardView.scaleY = 1f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start()
setupVideoControlsToggler()
}
companion object {
const val TAG = "VideoplayerFragment"
}
}

View File

@ -2,13 +2,14 @@ package ac.mdiq.podcini.ui.view
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.widget.VideoView
import kotlin.math.ceil
class AspectRatioVideoView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : VideoView(context, attrs, defStyle) {
defStyle: Int = 0)
: VideoView(context, attrs, defStyle) {
private var mVideoWidth = 0
private var mVideoHeight = 0
@ -20,7 +21,7 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
Log.d(TAG, "onMeasure $mAvailableWidth $mAvailableHeight")
if (mAvailableWidth < 0 || mAvailableHeight < 0) {
mAvailableWidth = width.toFloat()
mAvailableHeight = height.toFloat()
@ -54,6 +55,7 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
// Set the new video size
mVideoWidth = videoWidth
mVideoHeight = videoHeight
Log.d(TAG, "setVideoSize $mVideoWidth $mVideoHeight")
/*
* If this isn't set the video is stretched across the
@ -64,8 +66,8 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
*/
holder.setFixedSize(videoWidth, videoHeight)
requestLayout()
invalidate()
// requestLayout()
// invalidate()
}
/**
@ -76,6 +78,11 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
fun setAvailableSize(width: Float, height: Float) {
mAvailableWidth = width
mAvailableHeight = height
requestLayout()
Log.d(TAG, "setAvailableSize $mAvailableWidth $mAvailableHeight")
// requestLayout()
}
companion object {
private const val TAG = "AspectRatioVideoView"
}
}

View File

@ -53,7 +53,7 @@ object WidgetUpdater {
val views = RemoteViews(context.packageName, R.layout.player_widget)
if (widgetState.media != null) {
val icon: Bitmap?
var icon: Bitmap? = null
val iconSize = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer)
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer)
@ -65,26 +65,21 @@ object WidgetUpdater {
.transform(FitCenter(), RoundedCorners(radius))
try {
var imgLoc = widgetState.media.getImageLocation()
if (imgLoc != null) {
val imgLoc = widgetState.media.getImageLocation()
val imgLoc1 = getFallbackImageLocation(widgetState.media)
icon = Glide.with(context)
.asBitmap()
.load(imgLoc)
.error(Glide.with(context)
.asBitmap()
.load(imgLoc1)
.apply(options)
.submit(iconSize, iconSize)[500, TimeUnit.MILLISECONDS])
.apply(options)
.submit(iconSize, iconSize)
.get(500, TimeUnit.MILLISECONDS)
views.setImageViewBitmap(R.id.imgvCover, icon)
} else {
imgLoc = getFallbackImageLocation(widgetState.media)
if (imgLoc != null) {
icon = Glide.with(context)
.asBitmap()
.load(imgLoc)
.apply(options)
.submit(iconSize, iconSize)[500, TimeUnit.MILLISECONDS]
views.setImageViewBitmap(R.id.imgvCover, icon)
} else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher)
}
if (icon != null) views.setImageViewBitmap(R.id.imgvCover, icon)
else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher)
} catch (tr1: Throwable) {
Log.e(TAG, "Error loading the media icon for the widget", tr1)
}

View File

@ -0,0 +1,10 @@
<vector android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF"
android:strokeColor="#505050"
android:strokeWidth="0.75"
android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
</vector>

View File

@ -0,0 +1,196 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black"
android:id="@+id/videoEpisodeContainer">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black"
android:id="@+id/videoPlayerContainer">
<ac.mdiq.podcini.ui.view.AspectRatioVideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminateOnly="true"
android:visibility="invisible" />
<LinearLayout
android:id="@+id/controlsContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layoutDirection="ltr"
android:background="@android:color/transparent"
android:padding="16dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/audioOnly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/player_switch_to_audio_only"
app:srcCompat="@drawable/baseline_audiotrack_24" />
<ImageButton
android:id="@+id/rewindButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/rewind_label"
app:srcCompat="@drawable/ic_fast_rewind_video_white" />
<ac.mdiq.podcini.ui.view.PlayButton
android:id="@+id/playButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pause_label"
app:srcCompat="@drawable/ic_pause_video_white" />
<ImageButton
android:id="@+id/fastForwardButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/fast_forward_label"
app:srcCompat="@drawable/ic_fast_forward_video_white" />
<ImageButton
android:id="@+id/toggleViews"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/toggle_video_views"
app:srcCompat="@drawable/baseline_fullscreen_24" />
</LinearLayout>
<ImageView
android:id="@+id/skipAnimationImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:padding="64dp"
android:layout_gravity="center"/>
<LinearLayout
android:id="@+id/bottomControlsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/seekCardView"
android:alpha="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_gravity="center"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="?attr/seek_background"
app:cardElevation="0dp"
tools:alpha="1">
<TextView
android:id="@+id/seekPositionLabel"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="24dp"
android:paddingTop="4dp"
android:paddingRight="24dp"
android:paddingBottom="4dp"
android:textColor="@color/white"
android:textSize="24sp"
tools:text="1:06:29" />
</androidx.cardview.widget.CardView>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#80000000"
android:layoutDirection="ltr"
android:paddingTop="8dp">
<TextView
android:id="@+id/positionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:text="@string/position_default_label"
android:textColor="@color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/durationLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:text="@string/position_default_label"
android:textColor="@color/white"
android:textStyle="bold" />
<SeekBar
android:id="@+id/sbPosition"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/durationLabel"
android:layout_toStartOf="@+id/durationLabel"
android:layout_toRightOf="@+id/positionLabel"
android:layout_toEndOf="@+id/positionLabel"
android:layout_centerInParent="true"
android:max="500" />
</RelativeLayout>
</LinearLayout>
</FrameLayout>
<ac.mdiq.podcini.ui.view.ShownotesWebView
android:id="@+id/webvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/videoPlayerContainer"
android:foreground="?android:windowContentOverlay" />
</RelativeLayout>

View File

@ -5,158 +5,15 @@
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black"
android:orientation="vertical"
android:id="@+id/videoPlayerContainer">
android:id="@+id/videoPlayerContainer"
android:windowActionBarOverlay="true">
<ac.mdiq.podcini.ui.view.AspectRatioVideoView
android:id="@+id/videoView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminateOnly="true"
android:visibility="invisible" />
<LinearLayout
android:id="@+id/controlsContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layoutDirection="ltr"
android:background="@android:color/transparent"
android:padding="16dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/rewindButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/rewind_label"
app:srcCompat="@drawable/ic_fast_rewind_video_white" />
<ac.mdiq.podcini.ui.view.PlayButton
android:id="@+id/playButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pause_label"
app:srcCompat="@drawable/ic_pause_video_white" />
<ImageButton
android:id="@+id/fastForwardButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/fast_forward_label"
app:srcCompat="@drawable/ic_fast_forward_video_white" />
</LinearLayout>
<ImageView
android:id="@+id/skipAnimationImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:padding="64dp"
android:layout_gravity="center"/>
<LinearLayout
android:id="@+id/bottomControlsContainer"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/seekCardView"
android:alpha="0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_gravity="center"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="?attr/seek_background"
app:cardElevation="0dp"
tools:alpha="1">
<TextView
android:id="@+id/seekPositionLabel"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="24dp"
android:paddingTop="4dp"
android:paddingRight="24dp"
android:paddingBottom="4dp"
android:textColor="@color/white"
android:textSize="24sp"
tools:text="1:06:29" />
</androidx.cardview.widget.CardView>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#80000000"
android:layoutDirection="ltr"
android:paddingTop="8dp">
<TextView
android:id="@+id/positionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:text="@string/position_default_label"
android:textColor="@color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/durationLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:text="@string/position_default_label"
android:textColor="@color/white"
android:textStyle="bold" />
<SeekBar
android:id="@+id/sbPosition"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/durationLabel"
android:layout_toStartOf="@+id/durationLabel"
android:layout_toRightOf="@+id/positionLabel"
android:layout_toEndOf="@+id/positionLabel"
android:layout_centerInParent="true"
android:max="500" />
</RelativeLayout>
</LinearLayout>
android:layout_height="match_parent"
android:foreground="?android:windowContentOverlay"
tools:background="@android:color/holo_red_dark"
android:windowActionBarOverlay="true"/>
</FrameLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -177,6 +177,17 @@
<item>@string/add_feed_label</item>
</string-array>
<string-array name="video_mode_options">
<item>@string/pref_video_mode_small_window</item>
<item>@string/pref_video_mode_full_screen</item>
<item>@string/pref_video_mode_audio_only</item>
</string-array>
<string-array name="video_mode_options_values">
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
<string-array name="nav_drawer_feed_order_options">
<item>@string/drawer_feed_order_unplayed_episodes</item>
<item>@string/drawer_feed_order_alphabetical</item>

View File

@ -487,6 +487,11 @@
<string name="pref_fast_forward_sum">Customize the number of seconds to jump forward when the fast forward button is clicked</string>
<string name="pref_rewind">Rewind skip time</string>
<string name="pref_rewind_sum">Customize the number of seconds to jump backwards when the rewind button is clicked</string>
<string name="pref_playback_video_mode">Video play mode</string>
<string name="pref_playback_video_mode_sum">Choose how video episode is played by default</string>
<string name="pref_video_mode_full_screen">Full screen</string>
<string name="pref_video_mode_small_window">Small window</string>
<string name="pref_video_mode_audio_only">Audio only</string>
<string name="pref_expandNotify_title">High notification priority</string>
<string name="pref_expandNotify_sum">This usually expands the notification to show playback buttons.</string>
<string name="pref_persistNotify_title">Persistent playback controls</string>
@ -688,6 +693,7 @@
<string name="toolbar_back_button_content_description">Back</string>
<string name="rewind_label">Rewind</string>
<string name="fast_forward_label">Fast forward</string>
<string name="toggle_video_views">Toggle video views</string>
<string name="increase_speed">Increase speed</string>
<string name="decrease_speed">Decrease speed</string>
<string name="media_type_video_label">Video</string>

View File

@ -227,6 +227,16 @@
<item name="windowSplashScreenAnimatedIcon">@drawable/launcher_animate</item>
</style>
<style name="Theme.Podcini.VideoEpisode" parent="@style/Theme.Podcini.Dark">
<item name="android:fitsSystemWindows">true</item>
<!-- <item name="android:windowActionModeOverlay">true</item>-->
<!-- <item name="android:windowActionBarOverlay">true</item>-->
<!-- <item name="android:windowActionBar">true</item>-->
<!-- <item name="windowActionModeOverlay">true</item>-->
<!-- <item name="windowActionBar">true</item>-->
<item name="windowActionBarOverlay">true</item>
</style>
<style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
<item name="windowActionBarOverlay">true</item>
</style>

View File

@ -61,6 +61,11 @@
android:key="prefStreamOverDownload"
android:summary="@string/pref_stream_over_download_sum"
android:title="@string/pref_stream_over_download_title"/>
<Preference
android:title="@string/pref_playback_video_mode"
android:key="prefPlaybackVideoModeLauncher"
android:summary="@string/pref_playback_video_mode_sum"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/reassign_hardware_buttons">

View File

@ -44,7 +44,7 @@
android:title="@string/pref_nav_drawer_feed_counter_title"
android:key="prefDrawerFeedIndicator"
android:summary="@string/pref_nav_drawer_feed_counter_sum"
android:defaultValue="1"/>
android:defaultValue="2"/>
<!-- <Preference-->
<!-- android:title="@string/pref_filter_feed_title"-->
<!-- android:key="prefSubscriptionsFilter"-->

View File

@ -261,3 +261,15 @@
* fixed bug of receiving null view in function requiring non-null in subscriptions page
* set Counter default to number of SHOW_UNPLAYED
* removed FeedCounter.SHOW_NEW, NewEpisodesNotification, and associated notifications settings
## 4.8.0
* fixed empty player detailed view on first start
* player detailed view scrolls to the top on a new episode
* created video episode view, with video player on top and episode descriptions in portrait mode
* added video player mode setting in preferences, set to small window by default
* added on video controller easy switches to other video mode or audio only
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
* webkit updated to Androidx
* fixed bug in setting speed to wrong categories
* improved fetching of episode images when invalid addresses are given

View File

@ -0,0 +1,12 @@
Version 4.8.0 brings several changes:
* fixed empty player detailed view on first start
* player detailed view scrolls to the top on a new episode
* created video episode view, with video player on top and episode descriptions in portrait mode
* added video player mode setting in preferences, set to small window by default
* added on video controller easy switches to other video mode or audio only
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
* webkit updated to Androidx
* fixed bug in setting speed to wrong categories
* improved fetching of episode images when invalid addresses are given