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 * enabled intro- and end- skipping
* mark as played when finished * mark as played when finished
* streamed media is added to queue and is resumed after restart * 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 ### Podcast/Episode list

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 3020129 versionCode 3020130
versionName "4.7.1" versionName "4.8.0"
def commit = "" def commit = ""
try { try {
@ -238,6 +238,7 @@ dependencies {
implementation "androidx.work:work-runtime:2.9.0" implementation "androidx.work:work-runtime:2.9.0"
implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.core:core-splashscreen:1.0.1"
implementation 'androidx.documentfile:documentfile: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" implementation "com.google.android.material:material:1.11.0"

View File

@ -246,11 +246,12 @@
android:value="ac.mdiq.podcini.ui.activity.PreferenceActivity"/> android:value="ac.mdiq.podcini.ui.activity.PreferenceActivity"/>
</activity> </activity>
<!-- android:screenOrientation="sensorLandscape"-->
<activity <activity
android:name=".ui.activity.VideoplayerActivity" android:name=".ui.activity.VideoplayerActivity"
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize" android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
android:screenOrientation="sensorLandscape"
android:exported="false"> android:exported="false">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" 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() { fun playPause() {
if (media == null) return if (media == null) return
if (playbackService == null) { if (playbackService == null) {

View File

@ -1626,6 +1626,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
mediaPlayer?.setPlaybackParams(speed, isSkipSilence) mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
} else { } else {
if (codeArray != null && codeArray.size == 3) { if (codeArray != null && codeArray.size == 3) {
Log.d(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
if (codeArray[2]) setPlaybackSpeed(speed) if (codeArray[2]) setPlaybackSpeed(speed)
if (codeArray[1]) { if (codeArray[1]) {
var item = (playable as? FeedMedia)?.item ?: currentitem 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 iconSize = (128 * context.resources.displayMetrics.density).toInt()
val options = RequestOptions().centerCrop() val options = RequestOptions().centerCrop()
try { try {
var imgLoc = playable?.getImageLocation() val imgLoc = playable?.getImageLocation()
when { val imgLoc1 = ImageResourceUtils.getFallbackImageLocation(playable!!)
!imgLoc.isNullOrBlank() -> { Log.d(TAG, "loadIcon imgLoc $imgLoc $imgLoc1")
cachedIcon = Glide.with(context) cachedIcon = Glide.with(context)
.asBitmap() .asBitmap()
.load(imgLoc) .load(imgLoc)
.apply(options) .error(Glide.with(context)
.submit(iconSize, iconSize)
.get()
}
playable != null -> {
imgLoc = ImageResourceUtils.getFallbackImageLocation(playable!!)
if (!imgLoc.isNullOrBlank()) {
cachedIcon = Glide.with(context)
.asBitmap() .asBitmap()
.load(imgLoc) .load(imgLoc1)
.apply(options)
.submit(iconSize, iconSize)
.get())
.apply(options) .apply(options)
.submit(iconSize, iconSize) .submit(iconSize, iconSize)
.get() .get()
}
}
}
} catch (ignore: InterruptedException) { } catch (ignore: InterruptedException) {
Log.e(TAG, "Media icon loader was interrupted") Log.e(TAG, "Media icon loader was interrupted")
} catch (tr: Throwable) { } catch (tr: Throwable) {

View File

@ -111,6 +111,7 @@ object UserPreferences {
private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs" private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"
private const val PREF_REWIND_SECS = "prefRewindSecs" private const val PREF_REWIND_SECS = "prefRewindSecs"
private const val PREF_QUEUE_LOCKED = "prefQueueLocked" private const val PREF_QUEUE_LOCKED = "prefQueueLocked"
private const val PREF_VIDEO_MODE = "prefVideoPlaybackMode"
// Experimental // Experimental
const val EPISODE_CLEANUP_QUEUE: Int = -1 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 @JvmStatic
var videoPlaybackSpeed: Float var videoPlaybackSpeed: Float
get() { get() {
@ -661,6 +673,13 @@ object UserPreferences {
.apply() .apply()
} }
@JvmStatic
fun setVideoMode(mode: Int) {
prefs.edit()
.putString(PREF_VIDEO_MODE, mode.toString())
.apply()
}
@JvmStatic @JvmStatic
fun setAutodownloadSelectedNetworks(value: Array<String?>?) { fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
prefs.edit() 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.UsageStatistics.doNotAskAgain
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.dialog.EditFallbackSpeedDialog import ac.mdiq.podcini.ui.dialog.*
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.util.event.UnreadItemsUpdateEvent import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
@ -47,6 +44,12 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null) SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true true
} }
findPreference<Preference>(PREF_PLAYBACK_VIDEO_MODE_LAUNCHER)?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
VideoModeDialog.showDialog(requireContext())
true
}
findPreference<Preference>(PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER)!!.onPreferenceClickListener = findPreference<Preference>(PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER)!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
EditForwardSpeedDialog(requireActivity()).show() EditForwardSpeedDialog(requireActivity()).show()
@ -138,5 +141,6 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
private const val PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER = "prefPlaybackSpeedForwardLauncher" private const val PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER = "prefPlaybackSpeedForwardLauncher"
private const val PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher" private const val PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"
private const val PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload" 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) { override fun onSlide(view: View, slideOffset: Float) {
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
// audioPlayer.scrollToTop()
// }
audioPlayer.fadePlayerToToolbar(slideOffset) audioPlayer.fadePlayerToToolbar(slideOffset)
} }
} }

View File

@ -11,7 +11,7 @@ class PlaybackSpeedDialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getTranslucentTheme(this)) setTheme(getTranslucentTheme(this))
super.onCreate(savedInstanceState) 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) speedDialog?.show(supportFragmentManager, null)
} }

View File

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

View File

@ -1,107 +1,106 @@
package ac.mdiq.podcini.ui.activity package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R 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.getPlayerActivityIntent
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting 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.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.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.playback.cast.CastEnabledActivity import ac.mdiq.podcini.ui.fragment.ChaptersFragment
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting import ac.mdiq.podcini.util.FeedItemUtil.getLinkWithFallback
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
import ac.mdiq.podcini.util.event.MessageEvent import ac.mdiq.podcini.util.event.MessageEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent 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.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.view.View.OnTouchListener import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.view.animation.*
import android.widget.EditText 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 androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
/** /**
* Activity for playing video files. * Activity for playing video files.
*/ */
@UnstableApi @UnstableApi
class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener { class VideoplayerActivity : CastEnabledActivity() {
private var _binding: VideoplayerActivityBinding? = null private var _binding: VideoplayerActivityBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
/** lateinit var videoEpisodeFragment: VideoEpisodeFragment
* True if video controls are currently visible.
*/ var videoMode = 0
private var videoControlsShowing = true var switchToAudioOnly = false
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
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 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) window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
// has to be called before setting layout content // has to be called before setting layout content
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY) supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
setTheme(R.style.Theme_Podcini_VideoPlayer) 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) super.onCreate(savedInstanceState)
window.setFormat(PixelFormat.TRANSPARENT)
_binding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this)) _binding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root) setContentView(binding.root)
setupView()
supportActionBar?.setBackgroundDrawable(ColorDrawable(-0x80000000)) supportActionBar?.setBackgroundDrawable(ColorDrawable(-0x80000000))
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
controller = newPlaybackController() val fm = supportFragmentManager
controller!!.init() val transaction = fm.beginTransaction()
loadMediaInfo() videoEpisodeFragment = VideoEpisodeFragment()
transaction.replace(R.id.main_view, videoEpisodeFragment, VideoEpisodeFragment.TAG)
transaction.commit()
} }
@UnstableApi @UnstableApi
@ -111,7 +110,7 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
if (isCasting) { if (isCasting) {
val intent = getPlayerActivityIntent(this) val intent = getPlayerActivityIntent(this)
if (intent.component?.className != VideoplayerActivity::class.java.name) { if (intent.component?.className != VideoplayerActivity::class.java.name) {
destroyingDueToReload = true videoEpisodeFragment.destroyingDueToReload = true
finish() finish()
startActivity(intent) startActivity(intent)
} }
@ -120,21 +119,17 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
override fun onDestroy() { override fun onDestroy() {
super.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 _binding = null
controller?.release()
controller = null // prevent leak
disposable?.dispose()
} }
@UnstableApi @UnstableApi
override fun onStop() { override fun onStop() {
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
super.onStop() 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() { public override fun onUserLeaveHint() {
@ -146,20 +141,9 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
@UnstableApi @UnstableApi
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
onPositionObserverUpdate()
EventBus.getDefault().register(this) 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) { override fun onTrimMemory(level: Int) {
super.onTrimMemory(level) super.onTrimMemory(level)
Glide.get(this).trimMemory(level) Glide.get(this).trimMemory(level)
@ -170,47 +154,11 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
Glide.get(this).clearMemory() Glide.get(this).clearMemory()
} }
@UnstableApi fun toggleViews() {
private fun newPlaybackController(): PlaybackController { val newIntent = Intent(this, VideoplayerActivity::class.java)
return object : PlaybackController(this@VideoplayerActivity) { newIntent.putExtra("fullScreenMode", if (videoMode == FULL_SCREEN_VIEW) WINDOW_VIEW else FULL_SCREEN_VIEW)
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() {
finish() finish()
} startActivity(newIntent)
}
}
@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()
}
}
} }
@Subscribe(threadMode = ThreadMode.MAIN) @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) @Subscribe(threadMode = ThreadMode.MAIN)
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
@ -516,9 +204,9 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
@UnstableApi @UnstableApi
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {
super.onPrepareOptionsMenu(menu) 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) val isFeedMedia = (media is FeedMedia)
menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia) // FeedMedia implies it belongs to a Feed 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.add_to_favorites_item).setVisible(false)
menu.findItem(R.id.remove_from_favorites_item).setVisible(false) menu.findItem(R.id.remove_from_favorites_item).setVisible(false)
if (isFeedMedia) { if (isFeedMedia) {
menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite) menu.findItem(R.id.add_to_favorites_item).setVisible(!videoEpisodeFragment.isFavorite)
menu.findItem(R.id.remove_from_favorites_item).setVisible(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.set_sleeptimer_item).setVisible(!controller.sleepTimerActive())
menu.findItem(R.id.disable_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.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.playback_speed).setVisible(true)
menu.findItem(R.id.player_show_chapters).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 return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
val controller = videoEpisodeFragment.controller
// some options option requires FeedItem // some options option requires FeedItem
when { when {
item.itemId == R.id.player_switch_to_audio_only -> { item.itemId == R.id.player_switch_to_audio_only -> {
@ -571,17 +270,17 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
return false return false
} }
else -> { else -> {
val media = controller?.getMedia() ?: return false val media = controller.getMedia() ?: return false
val feedItem = getFeedItem(media) // some options option requires FeedItem val feedItem = getFeedItem(media) // some options option requires FeedItem
when { when {
item.itemId == R.id.add_to_favorites_item && feedItem != null -> { item.itemId == R.id.add_to_favorites_item && feedItem != null -> {
DBWriter.addFavoriteItem(feedItem) DBWriter.addFavoriteItem(feedItem)
isFavorite = true videoEpisodeFragment.isFavorite = true
invalidateOptionsMenu() invalidateOptionsMenu()
} }
item.itemId == R.id.remove_from_favorites_item && feedItem != null -> { item.itemId == R.id.remove_from_favorites_item && feedItem != null -> {
DBWriter.removeFavoriteItem(feedItem) DBWriter.removeFavoriteItem(feedItem)
isFavorite = false videoEpisodeFragment.isFavorite = false
invalidateOptionsMenu() invalidateOptionsMenu()
} }
item.itemId == R.id.disable_sleeptimer_item || item.itemId == R.id.set_sleeptimer_item -> { 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() { private fun compatEnterPictureInPicture() {
if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
supportActionBar?.hide() if (videoMode == FULL_SCREEN_VIEW) supportActionBar?.hide()
hideVideoControls(false) videoEpisodeFragment.hideVideoControls(false)
enterPictureInPictureMode() enterPictureInPictureMode()
} }
} }
@ -715,18 +331,18 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE -> { KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE -> {
onPlayPause() videoEpisodeFragment.onPlayPause()
toggleVideoControlsVisibility() videoEpisodeFragment.toggleVideoControlsVisibility()
return true return true
} }
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> { KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> {
onRewind() videoEpisodeFragment.onRewind()
showSkipAnimation(false) videoEpisodeFragment.showSkipAnimation(false)
return true return true
} }
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> { KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> {
onFastForward() videoEpisodeFragment.onFastForward()
showSkipAnimation(true) videoEpisodeFragment.showSkipAnimation(true)
return true return true
} }
KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_ESCAPE -> { KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_ESCAPE -> {
@ -756,7 +372,8 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
} }
//Go to x% of video: //Go to x% of video:
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { 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 true
} }
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
@ -764,23 +381,26 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
companion object { companion object {
private const val TAG = "VideoplayerActivity" 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? { private fun getWebsiteLinkWithFallback(media: Playable?): String? {
when { return when {
media == null -> { media == null -> {
return null null
} }
!media.getWebsiteLink().isNullOrBlank() -> { !media.getWebsiteLink().isNullOrBlank() -> {
return media.getWebsiteLink() media.getWebsiteLink()
} }
media is FeedMedia -> { 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) { return if (playable is FeedMedia) {
playable.item playable.item
} else { } else {

View File

@ -104,9 +104,6 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
binding.global.isChecked = true 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 = binding.speedSeekBar
speedSeekBar.setProgressChangedListener { multiplier: Float -> speedSeekBar.setProgressChangedListener { multiplier: Float ->
@ -184,9 +181,11 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
true true
} }
holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({ holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({
if (binding.currentAudio.isChecked) settingCode[0] = true Log.d("VariableSpeedDialog", "holder.chip settingCode0: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (binding.currentPodcast.isChecked) settingCode[1] = true settingCode[0] = binding.currentAudio.isChecked
if (binding.global.isChecked) settingCode[2] = true 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) { if (controller != null) {
dismiss() dismiss()
@ -203,13 +202,17 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
return selectedSpeeds[position].hashCode().toLong() return selectedSpeeds[position].hashCode().toLong()
} }
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder( inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
chip)
} }
companion object { 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? { 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) { if (settingCode.size != 3) {
Log.e("VariableSpeedDialog", "wrong settingCode dimension") Log.e("VariableSpeedDialog", "wrong settingCode dimension")
return null 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.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.PlaybackController.Companion
import ac.mdiq.podcini.playback.PlaybackServiceStarter
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.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.playback.service.PlaybackService 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.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
@ -23,6 +26,7 @@ import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler 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.ChapterSeekBar
import ac.mdiq.podcini.ui.view.PlayButton import ac.mdiq.podcini.ui.view.PlayButton
import ac.mdiq.podcini.util.ChapterUtils import ac.mdiq.podcini.util.ChapterUtils
@ -97,7 +101,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private var currentMedia: Playable? = null private var currentMedia: Playable? = null
private var currentitem: FeedItem? = null private var currentitem: FeedItem? = null
@SuppressLint("WrongConstant")
override fun onCreateView(inflater: LayoutInflater, override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
@ -447,6 +450,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
} }
@JvmOverloads
fun scrollToTop() {
itemDescFrag.scrollToTop()
}
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 = playerView1 val player = playerView1
@ -515,9 +523,12 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
Log.d(TAG, "internalPlayerFragment was clicked") Log.d(TAG, "internalPlayerFragment was clicked")
val media = controller?.getMedia() val media = controller?.getMedia()
if (media != null) { 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) (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else { } else {
controller?.playPause()
// controller!!.ensureService()
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media) val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media)
startActivity(intent) startActivity(intent)
} }
@ -712,19 +723,17 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
.fitCenter() .fitCenter()
.dontAnimate() .dontAnimate()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media) val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media) + "sdfsdf"
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media) val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
when {
!imgLoc.isNullOrBlank() -> Glide.with(this) Glide.with(this)
.load(imgLoc) .load(imgLoc)
.apply(options) .error(Glide.with(this)
.into(imgvCover)
!imgLocFB.isNullOrBlank() -> Glide.with(this)
.load(imgLocFB) .load(imgLocFB)
.error(R.mipmap.ic_launcher)
.apply(options))
.apply(options) .apply(options)
.into(imgvCover) .into(imgvCover)
else -> imgvCover.setImageResource(R.mipmap.ic_launcher)
}
if (controller?.isPlayingVideoLocally == true) { if (controller?.isPlayingVideoLocally == true) {
(activity as MainActivity).bottomSheet.setLocked(true) (activity as MainActivity).bottomSheet.setLocked(true)
@ -747,7 +756,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
} }
companion object { companion object {
const val TAG: String = "AudioPlayerFragment" const val TAG: String = "AudioPlayerFragment"
} }

View File

@ -316,7 +316,6 @@ class PlayerDetailsFragment : Fragment() {
@UnstableApi private fun seekToPrevChapter() { @UnstableApi private fun seekToPrevChapter() {
val curr: Chapter? = currentChapter val curr: Chapter? = currentChapter
if (controller == null || curr == null || displayedChapterIndex == -1) return if (controller == null || curr == null || displayedChapterIndex == -1) return
when { when {
@ -353,8 +352,8 @@ class PlayerDetailsFragment : Fragment() {
val prefs = requireActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE) val prefs = requireActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE)
val editor = prefs.edit() val editor = prefs.edit()
if (controller?.getMedia() != null) { if (controller?.getMedia() != null) {
Log.d(TAG, "Saving scroll position: " + webvDescription.scrollY) Log.d(TAG, "Saving scroll position: " + binding.itemDescriptionFragment.scrollY)
editor.putInt(PREF_SCROLL_Y, webvDescription.scrollY) editor.putInt(PREF_SCROLL_Y, binding.itemDescriptionFragment.scrollY)
editor.putString(PREF_PLAYABLE_ID, controller!!.getMedia()!!.getIdentifier().toString()) editor.putString(PREF_PLAYABLE_ID, controller!!.getMedia()!!.getIdentifier().toString())
} else { } else {
Log.d(TAG, "savePreferences was called while media or webview was null") Log.d(TAG, "savePreferences was called while media or webview was null")
@ -374,11 +373,12 @@ class PlayerDetailsFragment : Fragment() {
if (scrollY != -1) { if (scrollY != -1) {
if (id == controller?.getMedia()?.getIdentifier()?.toString()) { if (id == controller?.getMedia()?.getIdentifier()?.toString()) {
Log.d(TAG, "Restored scroll Position: $scrollY") Log.d(TAG, "Restored scroll Position: $scrollY")
webvDescription.scrollTo(webvDescription.scrollX, scrollY) binding.itemDescriptionFragment.scrollTo(binding.itemDescriptionFragment.scrollX, scrollY)
return true return true
} }
Log.d(TAG, "reset scroll Position: 0") Log.d(TAG, "reset scroll Position: 0")
webvDescription.scrollTo(webvDescription.scrollX, 0) binding.itemDescriptionFragment.scrollTo(0, 0)
return true return true
} }
} }
@ -386,7 +386,7 @@ class PlayerDetailsFragment : Fragment() {
} }
fun scrollToTop() { fun scrollToTop() {
webvDescription.scrollTo(0, 0) binding.itemDescriptionFragment.scrollTo(0, 0)
savePreference() 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.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.widget.VideoView import android.widget.VideoView
import kotlin.math.ceil import kotlin.math.ceil
class AspectRatioVideoView @JvmOverloads constructor(context: Context, class AspectRatioVideoView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0 defStyle: Int = 0)
) : VideoView(context, attrs, defStyle) { : VideoView(context, attrs, defStyle) {
private var mVideoWidth = 0 private var mVideoWidth = 0
private var mVideoHeight = 0 private var mVideoHeight = 0
@ -20,7 +21,7 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return return
} }
Log.d(TAG, "onMeasure $mAvailableWidth $mAvailableHeight")
if (mAvailableWidth < 0 || mAvailableHeight < 0) { if (mAvailableWidth < 0 || mAvailableHeight < 0) {
mAvailableWidth = width.toFloat() mAvailableWidth = width.toFloat()
mAvailableHeight = height.toFloat() mAvailableHeight = height.toFloat()
@ -54,6 +55,7 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
// Set the new video size // Set the new video size
mVideoWidth = videoWidth mVideoWidth = videoWidth
mVideoHeight = videoHeight mVideoHeight = videoHeight
Log.d(TAG, "setVideoSize $mVideoWidth $mVideoHeight")
/* /*
* If this isn't set the video is stretched across the * 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) holder.setFixedSize(videoWidth, videoHeight)
requestLayout() // requestLayout()
invalidate() // invalidate()
} }
/** /**
@ -76,6 +78,11 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
fun setAvailableSize(width: Float, height: Float) { fun setAvailableSize(width: Float, height: Float) {
mAvailableWidth = width mAvailableWidth = width
mAvailableHeight = height 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) val views = RemoteViews(context.packageName, R.layout.player_widget)
if (widgetState.media != null) { if (widgetState.media != null) {
val icon: Bitmap? var icon: Bitmap? = null
val iconSize = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size) val iconSize = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer) views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer)
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer) views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer)
@ -65,26 +65,21 @@ object WidgetUpdater {
.transform(FitCenter(), RoundedCorners(radius)) .transform(FitCenter(), RoundedCorners(radius))
try { try {
var imgLoc = widgetState.media.getImageLocation() val imgLoc = widgetState.media.getImageLocation()
if (imgLoc != null) { val imgLoc1 = getFallbackImageLocation(widgetState.media)
icon = Glide.with(context) icon = Glide.with(context)
.asBitmap() .asBitmap()
.load(imgLoc) .load(imgLoc)
.error(Glide.with(context)
.asBitmap()
.load(imgLoc1)
.apply(options)
.submit(iconSize, iconSize)[500, TimeUnit.MILLISECONDS])
.apply(options) .apply(options)
.submit(iconSize, iconSize) .submit(iconSize, iconSize)
.get(500, TimeUnit.MILLISECONDS) .get(500, TimeUnit.MILLISECONDS)
views.setImageViewBitmap(R.id.imgvCover, icon) if (icon != null) views.setImageViewBitmap(R.id.imgvCover, icon)
} else { else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher)
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)
}
} catch (tr1: Throwable) { } catch (tr1: Throwable) {
Log.e(TAG, "Error loading the media icon for the widget", tr1) 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" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black" android:background="@color/black"
android:orientation="vertical" android:id="@+id/videoPlayerContainer"
android:id="@+id/videoPlayerContainer"> android:windowActionBarOverlay="true">
<ac.mdiq.podcini.ui.view.AspectRatioVideoView <androidx.fragment.app.FragmentContainerView
android:id="@+id/videoView" android:id="@+id/main_view"
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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="bottom|center" android:foreground="?android:windowContentOverlay"
android:orientation="vertical"> tools:background="@android:color/holo_red_dark"
android:windowActionBarOverlay="true"/>
<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> </FrameLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -177,6 +177,17 @@
<item>@string/add_feed_label</item> <item>@string/add_feed_label</item>
</string-array> </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"> <string-array name="nav_drawer_feed_order_options">
<item>@string/drawer_feed_order_unplayed_episodes</item> <item>@string/drawer_feed_order_unplayed_episodes</item>
<item>@string/drawer_feed_order_alphabetical</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_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">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_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_title">High notification priority</string>
<string name="pref_expandNotify_sum">This usually expands the notification to show playback buttons.</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> <string name="pref_persistNotify_title">Persistent playback controls</string>
@ -688,6 +693,7 @@
<string name="toolbar_back_button_content_description">Back</string> <string name="toolbar_back_button_content_description">Back</string>
<string name="rewind_label">Rewind</string> <string name="rewind_label">Rewind</string>
<string name="fast_forward_label">Fast forward</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="increase_speed">Increase speed</string>
<string name="decrease_speed">Decrease speed</string> <string name="decrease_speed">Decrease speed</string>
<string name="media_type_video_label">Video</string> <string name="media_type_video_label">Video</string>

View File

@ -227,6 +227,16 @@
<item name="windowSplashScreenAnimatedIcon">@drawable/launcher_animate</item> <item name="windowSplashScreenAnimatedIcon">@drawable/launcher_animate</item>
</style> </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"> <style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
<item name="windowActionBarOverlay">true</item> <item name="windowActionBarOverlay">true</item>
</style> </style>

View File

@ -61,6 +61,11 @@
android:key="prefStreamOverDownload" android:key="prefStreamOverDownload"
android:summary="@string/pref_stream_over_download_sum" android:summary="@string/pref_stream_over_download_sum"
android:title="@string/pref_stream_over_download_title"/> 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>
<PreferenceCategory android:title="@string/reassign_hardware_buttons"> <PreferenceCategory android:title="@string/reassign_hardware_buttons">

View File

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

View File

@ -261,3 +261,15 @@
* fixed bug of receiving null view in function requiring non-null in subscriptions page * fixed bug of receiving null view in function requiring non-null in subscriptions page
* set Counter default to number of SHOW_UNPLAYED * set Counter default to number of SHOW_UNPLAYED
* removed FeedCounter.SHOW_NEW, NewEpisodesNotification, and associated notifications settings * 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