4.8.0 commit
This commit is contained in:
parent
26115b436f
commit
1f8bb954a0
|
@ -60,6 +60,10 @@ Even so, the database remains backward compatible, and AntennaPod's db can be ea
|
|||
* enabled intro- and end- skipping
|
||||
* mark as played when finished
|
||||
* streamed media is added to queue and is resumed after restart
|
||||
* new video episode view, with video player on top and episode descriptions in portrait mode
|
||||
* easy switches on video player to other video mode or audio only
|
||||
* default video player mode setting in preferences
|
||||
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
|
||||
|
||||
### Podcast/Episode list
|
||||
|
||||
|
|
|
@ -149,8 +149,8 @@ android {
|
|||
// Version code schema (not used):
|
||||
// "1.2.3-beta4" -> 1020304
|
||||
// "1.2.3" -> 1020395
|
||||
versionCode 3020129
|
||||
versionName "4.7.1"
|
||||
versionCode 3020130
|
||||
versionName "4.8.0"
|
||||
|
||||
def commit = ""
|
||||
try {
|
||||
|
@ -238,6 +238,7 @@ dependencies {
|
|||
implementation "androidx.work:work-runtime:2.9.0"
|
||||
implementation "androidx.core:core-splashscreen:1.0.1"
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.webkit:webkit:1.9.0'
|
||||
|
||||
implementation "com.google.android.material:material:1.11.0"
|
||||
|
||||
|
|
|
@ -246,11 +246,12 @@
|
|||
android:value="ac.mdiq.podcini.ui.activity.PreferenceActivity"/>
|
||||
</activity>
|
||||
|
||||
<!-- android:screenOrientation="sensorLandscape"-->
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.VideoplayerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
|
|
|
@ -267,6 +267,14 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
}
|
||||
}
|
||||
|
||||
fun ensureService() {
|
||||
if (media == null) return
|
||||
if (playbackService == null) {
|
||||
PlaybackServiceStarter(activity, media!!).start()
|
||||
Log.w(TAG, "playbackservice was null, restarted!")
|
||||
}
|
||||
}
|
||||
|
||||
fun playPause() {
|
||||
if (media == null) return
|
||||
if (playbackService == null) {
|
||||
|
|
|
@ -1626,6 +1626,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||
} else {
|
||||
if (codeArray != null && codeArray.size == 3) {
|
||||
Log.d(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
|
||||
if (codeArray[2]) setPlaybackSpeed(speed)
|
||||
if (codeArray[1]) {
|
||||
var item = (playable as? FeedMedia)?.item ?: currentitem
|
||||
|
|
|
@ -62,28 +62,21 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
val iconSize = (128 * context.resources.displayMetrics.density).toInt()
|
||||
val options = RequestOptions().centerCrop()
|
||||
try {
|
||||
var imgLoc = playable?.getImageLocation()
|
||||
when {
|
||||
!imgLoc.isNullOrBlank() -> {
|
||||
val imgLoc = playable?.getImageLocation()
|
||||
val imgLoc1 = ImageResourceUtils.getFallbackImageLocation(playable!!)
|
||||
Log.d(TAG, "loadIcon imgLoc $imgLoc $imgLoc1")
|
||||
cachedIcon = Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgLoc)
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)
|
||||
.get()
|
||||
}
|
||||
playable != null -> {
|
||||
imgLoc = ImageResourceUtils.getFallbackImageLocation(playable!!)
|
||||
if (!imgLoc.isNullOrBlank()) {
|
||||
cachedIcon = Glide.with(context)
|
||||
.error(Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgLoc)
|
||||
.load(imgLoc1)
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)
|
||||
.get())
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)
|
||||
.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ignore: InterruptedException) {
|
||||
Log.e(TAG, "Media icon loader was interrupted")
|
||||
} catch (tr: Throwable) {
|
||||
|
|
|
@ -111,6 +111,7 @@ object UserPreferences {
|
|||
private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"
|
||||
private const val PREF_REWIND_SECS = "prefRewindSecs"
|
||||
private const val PREF_QUEUE_LOCKED = "prefQueueLocked"
|
||||
private const val PREF_VIDEO_MODE = "prefVideoPlaybackMode"
|
||||
|
||||
// Experimental
|
||||
const val EPISODE_CLEANUP_QUEUE: Int = -1
|
||||
|
@ -410,6 +411,17 @@ object UserPreferences {
|
|||
}
|
||||
}
|
||||
|
||||
val videoPlayMode: Int
|
||||
get() {
|
||||
try {
|
||||
return prefs.getString(PREF_VIDEO_MODE, "1")!!.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
setVideoMode(1)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
var videoPlaybackSpeed: Float
|
||||
get() {
|
||||
|
@ -661,6 +673,13 @@ object UserPreferences {
|
|||
.apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setVideoMode(mode: Int) {
|
||||
prefs.edit()
|
||||
.putString(PREF_VIDEO_MODE, mode.toString())
|
||||
.apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
|
||||
prefs.edit()
|
||||
|
|
|
@ -5,10 +5,7 @@ import ac.mdiq.podcini.preferences.UsageStatistics
|
|||
import ac.mdiq.podcini.preferences.UsageStatistics.doNotAskAgain
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.dialog.EditFallbackSpeedDialog
|
||||
import ac.mdiq.podcini.ui.dialog.EditForwardSpeedDialog
|
||||
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||
import ac.mdiq.podcini.ui.dialog.*
|
||||
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
|
@ -47,6 +44,12 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
|
|||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_PLAYBACK_VIDEO_MODE_LAUNCHER)?.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
VideoModeDialog.showDialog(requireContext())
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>(PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER)!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
EditForwardSpeedDialog(requireActivity()).show()
|
||||
|
@ -138,5 +141,6 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
|
|||
private const val PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER = "prefPlaybackSpeedForwardLauncher"
|
||||
private const val PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"
|
||||
private const val PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload"
|
||||
private const val PREF_PLAYBACK_VIDEO_MODE_LAUNCHER = "prefPlaybackVideoModeLauncher"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -286,6 +286,9 @@ class MainActivity : CastEnabledActivity() {
|
|||
}
|
||||
override fun onSlide(view: View, slideOffset: Float) {
|
||||
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
|
||||
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
|
||||
// audioPlayer.scrollToTop()
|
||||
// }
|
||||
audioPlayer.fadePlayerToToolbar(slideOffset)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class PlaybackSpeedDialogActivity : AppCompatActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTranslucentTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
val speedDialog: VariableSpeedDialog? = VariableSpeedDialog.newInstance(booleanArrayOf(false, false, true), 2)
|
||||
val speedDialog: VariableSpeedDialog? = VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), 2)
|
||||
speedDialog?.show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
|
|
|
@ -113,15 +113,13 @@ class SelectSubscriptionActivity : AppCompatActivity() {
|
|||
.apply(RequestOptions.overrideOf(iconSize, iconSize))
|
||||
.listener(object : RequestListener<Bitmap?> {
|
||||
@UnstableApi override fun onLoadFailed(e: GlideException?, model: Any?,
|
||||
target: Target<Bitmap?>, isFirstResource: Boolean
|
||||
): Boolean {
|
||||
target: Target<Bitmap?>, isFirstResource: Boolean): Boolean {
|
||||
addShortcut(feed, null)
|
||||
return true
|
||||
}
|
||||
|
||||
@UnstableApi override fun onResourceReady(resource: Bitmap, model: Any,
|
||||
target: Target<Bitmap?>, dataSource: DataSource, isFirstResource: Boolean
|
||||
): Boolean {
|
||||
target: Target<Bitmap?>, dataSource: DataSource, isFirstResource: Boolean): Boolean {
|
||||
addShortcut(feed, resource)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,107 +1,106 @@
|
|||
package ac.mdiq.podcini.ui.activity
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.storage.DBWriter
|
||||
import ac.mdiq.podcini.util.Converter.getDurationStringLong
|
||||
import ac.mdiq.podcini.util.FeedItemUtil.getLinkWithFallback
|
||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
|
||||
import ac.mdiq.podcini.util.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
||||
import ac.mdiq.podcini.playback.PlaybackController
|
||||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||
import ac.mdiq.podcini.ui.dialog.*
|
||||
import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent
|
||||
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
|
||||
import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent
|
||||
import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent
|
||||
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
||||
import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.*
|
||||
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
|
||||
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
|
||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
||||
import ac.mdiq.podcini.util.FeedItemUtil.getLinkWithFallback
|
||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
|
||||
import ac.mdiq.podcini.util.event.MessageEvent
|
||||
import ac.mdiq.podcini.util.event.PlayerErrorEvent
|
||||
import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent
|
||||
import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.animation.*
|
||||
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
||||
|
||||
/**
|
||||
* Activity for playing video files.
|
||||
*/
|
||||
@UnstableApi
|
||||
class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
||||
class VideoplayerActivity : CastEnabledActivity() {
|
||||
|
||||
private var _binding: VideoplayerActivityBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
/**
|
||||
* True if video controls are currently visible.
|
||||
*/
|
||||
private var videoControlsShowing = true
|
||||
private var videoSurfaceCreated = false
|
||||
private var destroyingDueToReload = false
|
||||
private var lastScreenTap: Long = 0
|
||||
private val videoControlsHider = Handler(Looper.getMainLooper())
|
||||
private var controller: PlaybackController? = null
|
||||
private var showTimeLeft = false
|
||||
private var isFavorite = false
|
||||
private var switchToAudioOnly = false
|
||||
private var disposable: Disposable? = null
|
||||
private var prog = 0f
|
||||
lateinit var videoEpisodeFragment: VideoEpisodeFragment
|
||||
|
||||
var videoMode = 0
|
||||
var switchToAudioOnly = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
videoMode = intent.getIntExtra("fullScreenMode",0)
|
||||
if (videoMode == 0) {
|
||||
videoMode = videoPlayMode
|
||||
if (videoMode == AUDIO_ONLY) {
|
||||
switchToAudioOnly = true
|
||||
finish()
|
||||
}
|
||||
if (videoMode != FULL_SCREEN_VIEW && videoMode != WINDOW_VIEW) {
|
||||
Log.i(TAG, "videoMode not selected, use window mode")
|
||||
videoMode = WINDOW_VIEW
|
||||
}
|
||||
}
|
||||
|
||||
when (videoMode) {
|
||||
FULL_SCREEN_VIEW -> {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
||||
// has to be called before setting layout content
|
||||
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
|
||||
setTheme(R.style.Theme_Podcini_VideoPlayer)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
window.setFormat(PixelFormat.TRANSPARENT)
|
||||
}
|
||||
WINDOW_VIEW -> {
|
||||
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
|
||||
setTheme(R.style.Theme_Podcini_VideoEpisode)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||
window.setFormat(PixelFormat.TRANSPARENT)
|
||||
}
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
window.setFormat(PixelFormat.TRANSPARENT)
|
||||
_binding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
setupView()
|
||||
supportActionBar?.setBackgroundDrawable(ColorDrawable(-0x80000000))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
controller = newPlaybackController()
|
||||
controller!!.init()
|
||||
loadMediaInfo()
|
||||
val fm = supportFragmentManager
|
||||
val transaction = fm.beginTransaction()
|
||||
videoEpisodeFragment = VideoEpisodeFragment()
|
||||
transaction.replace(R.id.main_view, videoEpisodeFragment, VideoEpisodeFragment.TAG)
|
||||
transaction.commit()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
|
@ -111,7 +110,7 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
if (isCasting) {
|
||||
val intent = getPlayerActivityIntent(this)
|
||||
if (intent.component?.className != VideoplayerActivity::class.java.name) {
|
||||
destroyingDueToReload = true
|
||||
videoEpisodeFragment.destroyingDueToReload = true
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
@ -120,21 +119,17 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||
_binding = null
|
||||
controller?.release()
|
||||
controller = null // prevent leak
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onStop() {
|
||||
EventBus.getDefault().unregister(this)
|
||||
super.onStop()
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
}
|
||||
// Controller released; we will not receive buffering updates
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
public override fun onUserLeaveHint() {
|
||||
|
@ -146,20 +141,9 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
@UnstableApi
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
onPositionObserverUpdate()
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onPause() {
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
|
||||
if (controller?.status == PlayerStatus.PLAYING) {
|
||||
controller!!.pause()
|
||||
}
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
Glide.get(this).trimMemory(level)
|
||||
|
@ -170,47 +154,11 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
Glide.get(this).clearMemory()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun newPlaybackController(): PlaybackController {
|
||||
return object : PlaybackController(this@VideoplayerActivity) {
|
||||
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
|
||||
binding.playButton.setIsShowPlay(showPlay)
|
||||
if (showPlay) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setupVideoAspectRatio()
|
||||
if (videoSurfaceCreated && controller != null) {
|
||||
Log.d(TAG, "Videosurface already created, setting videosurface now")
|
||||
controller!!.setVideoSurface(binding.videoView.holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadMediaInfo() {
|
||||
this@VideoplayerActivity.loadMediaInfo()
|
||||
}
|
||||
|
||||
override fun onPlaybackEnd() {
|
||||
fun toggleViews() {
|
||||
val newIntent = Intent(this, VideoplayerActivity::class.java)
|
||||
newIntent.putExtra("fullScreenMode", if (videoMode == FULL_SCREEN_VIEW) WINDOW_VIEW else FULL_SCREEN_VIEW)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@Suppress("unused")
|
||||
fun bufferUpdate(event: BufferUpdateEvent) {
|
||||
when {
|
||||
event.hasStarted() -> {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
event.hasEnded() -> {
|
||||
binding.progressBar.visibility = View.INVISIBLE
|
||||
}
|
||||
else -> {
|
||||
binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
|
||||
}
|
||||
}
|
||||
startActivity(newIntent)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
|
@ -221,266 +169,6 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun loadMediaInfo() {
|
||||
Log.d(TAG, "loadMediaInfo()")
|
||||
if (controller?.getMedia() == null) return
|
||||
|
||||
if (controller!!.status == PlayerStatus.PLAYING && !controller!!.isPlayingVideoLocally) {
|
||||
Log.d(TAG, "Closing, no longer video")
|
||||
destroyingDueToReload = true
|
||||
finish()
|
||||
MainActivityStarter(this).withOpenPlayer().start()
|
||||
return
|
||||
}
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
onPositionObserverUpdate()
|
||||
checkFavorite()
|
||||
val media = controller!!.getMedia()
|
||||
if (media != null) {
|
||||
supportActionBar!!.subtitle = media.getEpisodeTitle()
|
||||
supportActionBar!!.title = media.getFeedTitle()
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun setupView() {
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
Log.d("timeleft", if (showTimeLeft) "true" else "false")
|
||||
binding.durationLabel.setOnClickListener {
|
||||
showTimeLeft = !showTimeLeft
|
||||
val media = controller?.getMedia() ?: return@setOnClickListener
|
||||
|
||||
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
|
||||
val length: String
|
||||
if (showTimeLeft) {
|
||||
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
||||
length = "-" + getDurationStringLong(remainingTime)
|
||||
} else {
|
||||
val duration = converter.convert(media.getDuration())
|
||||
length = getDurationStringLong(duration)
|
||||
}
|
||||
binding.durationLabel.text = length
|
||||
|
||||
setShowRemainTimeSetting(showTimeLeft)
|
||||
Log.d("timeleft on click", if (showTimeLeft) "true" else "false")
|
||||
}
|
||||
|
||||
binding.sbPosition.setOnSeekBarChangeListener(this)
|
||||
binding.rewindButton.setOnClickListener { onRewind() }
|
||||
binding.rewindButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity,
|
||||
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||
true
|
||||
}
|
||||
binding.playButton.setIsVideoScreen(true)
|
||||
binding.playButton.setOnClickListener { onPlayPause() }
|
||||
binding.fastForwardButton.setOnClickListener { onFastForward() }
|
||||
binding.fastForwardButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity,
|
||||
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
||||
false
|
||||
}
|
||||
// To suppress touches directly below the slider
|
||||
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
|
||||
binding.bottomControlsContainer.fitsSystemWindows = true
|
||||
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
||||
binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
|
||||
setupVideoControlsToggler()
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
|
||||
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
||||
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
binding.videoView.setAvailableSize(
|
||||
binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private val hideVideoControls = Runnable {
|
||||
if (videoControlsShowing) {
|
||||
Log.d(TAG, "Hiding video controls")
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(true)
|
||||
videoControlsShowing = false
|
||||
}
|
||||
}
|
||||
|
||||
private val onVideoviewTouched = OnTouchListener { v: View, event: MotionEvent ->
|
||||
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
|
||||
|
||||
if (PictureInPictureUtil.isInPictureInPictureMode(this)) return@OnTouchListener true
|
||||
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
|
||||
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
||||
if (event.x > v.measuredWidth / 2.0f) {
|
||||
onFastForward()
|
||||
showSkipAnimation(true)
|
||||
} else {
|
||||
onRewind()
|
||||
showSkipAnimation(false)
|
||||
}
|
||||
if (videoControlsShowing) {
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(false)
|
||||
videoControlsShowing = false
|
||||
}
|
||||
return@OnTouchListener true
|
||||
}
|
||||
|
||||
toggleVideoControlsVisibility()
|
||||
if (videoControlsShowing) setupVideoControlsToggler()
|
||||
|
||||
lastScreenTap = System.currentTimeMillis()
|
||||
true
|
||||
}
|
||||
|
||||
private fun showSkipAnimation(isForward: Boolean) {
|
||||
val skipAnimation = AnimationSet(true)
|
||||
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
|
||||
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
|
||||
skipAnimation.fillAfter = false
|
||||
skipAnimation.duration = 800
|
||||
|
||||
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
|
||||
if (isForward) {
|
||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
|
||||
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
||||
} else {
|
||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
||||
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||
}
|
||||
|
||||
binding.skipAnimationImage.visibility = View.VISIBLE
|
||||
binding.skipAnimationImage.layoutParams = params
|
||||
binding.skipAnimationImage.startAnimation(skipAnimation)
|
||||
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
binding.skipAnimationImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupVideoControlsToggler() {
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
videoControlsHider.postDelayed(hideVideoControls, 2500)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun setupVideoAspectRatio() {
|
||||
if (videoSurfaceCreated && controller != null) {
|
||||
val videoSize = controller!!.videoSize
|
||||
if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) {
|
||||
Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second)
|
||||
binding.videoView.setVideoSize(videoSize.first, videoSize.second)
|
||||
} else {
|
||||
Log.e(TAG, "Could not determine video size")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleVideoControlsVisibility() {
|
||||
if (videoControlsShowing) {
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(true)
|
||||
} else {
|
||||
supportActionBar?.show()
|
||||
showVideoControls()
|
||||
}
|
||||
videoControlsShowing = !videoControlsShowing
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onRewind() {
|
||||
if (controller == null) return
|
||||
|
||||
val curr = controller!!.position
|
||||
controller!!.seekTo(curr - rewindSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onPlayPause() {
|
||||
if (controller == null) return
|
||||
|
||||
controller!!.playPause()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onFastForward() {
|
||||
if (controller == null) return
|
||||
|
||||
val curr = controller!!.position
|
||||
controller!!.seekTo(curr + fastForwardSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
holder.setFixedSize(width, height)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Log.d(TAG, "Videoview holder created")
|
||||
videoSurfaceCreated = true
|
||||
if (controller?.status == PlayerStatus.PLAYING) {
|
||||
controller!!.setVideoSurface(holder)
|
||||
}
|
||||
setupVideoAspectRatio()
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Log.d(TAG, "Videosurface was destroyed")
|
||||
videoSurfaceCreated = false
|
||||
if (controller != null && !destroyingDueToReload && !switchToAudioOnly) {
|
||||
controller!!.notifyVideoSurfaceAbandoned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVideoControls() {
|
||||
binding.bottomControlsContainer.visibility = View.VISIBLE
|
||||
binding.controlsContainer.visibility = View.VISIBLE
|
||||
val animation = AnimationUtils.loadAnimation(this, R.anim.fade_in)
|
||||
if (animation != null) {
|
||||
binding.bottomControlsContainer.startAnimation(animation)
|
||||
binding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
||||
}
|
||||
|
||||
private fun hideVideoControls(showAnimation: Boolean) {
|
||||
if (showAnimation) {
|
||||
val animation = AnimationUtils.loadAnimation(this, R.anim.fade_out)
|
||||
if (animation != null) {
|
||||
binding.bottomControlsContainer.startAnimation(animation)
|
||||
binding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
}
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||
binding.bottomControlsContainer.fitsSystemWindows = true
|
||||
|
||||
binding.bottomControlsContainer.visibility = View.GONE
|
||||
binding.controlsContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: PlaybackPositionEvent?) {
|
||||
onPositionObserverUpdate()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
|
||||
if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
|
||||
|
@ -516,9 +204,9 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
@UnstableApi
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
if (controller == null) return false
|
||||
val controller = videoEpisodeFragment.controller ?: return false
|
||||
|
||||
val media = controller!!.getMedia()
|
||||
val media = controller.getMedia()
|
||||
val isFeedMedia = (media is FeedMedia)
|
||||
|
||||
menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia) // FeedMedia implies it belongs to a Feed
|
||||
|
@ -533,22 +221,33 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
menu.findItem(R.id.add_to_favorites_item).setVisible(false)
|
||||
menu.findItem(R.id.remove_from_favorites_item).setVisible(false)
|
||||
if (isFeedMedia) {
|
||||
menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite)
|
||||
menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite)
|
||||
menu.findItem(R.id.add_to_favorites_item).setVisible(!videoEpisodeFragment.isFavorite)
|
||||
menu.findItem(R.id.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite)
|
||||
}
|
||||
|
||||
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller!!.sleepTimerActive())
|
||||
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller!!.sleepTimerActive())
|
||||
|
||||
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive())
|
||||
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive())
|
||||
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
|
||||
|
||||
menu.findItem(R.id.audio_controls).setVisible(controller!!.audioTracks.size >= 2)
|
||||
menu.findItem(R.id.audio_controls).setVisible(controller.audioTracks.size >= 2)
|
||||
menu.findItem(R.id.playback_speed).setVisible(true)
|
||||
menu.findItem(R.id.player_show_chapters).setVisible(true)
|
||||
|
||||
if (videoMode == WINDOW_VIEW) {
|
||||
menu.findItem(R.id.add_to_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.remove_from_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.set_sleeptimer_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.disable_sleeptimer_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.player_switch_to_audio_only).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.open_feed_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.share_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val controller = videoEpisodeFragment.controller
|
||||
|
||||
// some options option requires FeedItem
|
||||
when {
|
||||
item.itemId == R.id.player_switch_to_audio_only -> {
|
||||
|
@ -571,17 +270,17 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
return false
|
||||
}
|
||||
else -> {
|
||||
val media = controller?.getMedia() ?: return false
|
||||
val media = controller.getMedia() ?: return false
|
||||
val feedItem = getFeedItem(media) // some options option requires FeedItem
|
||||
when {
|
||||
item.itemId == R.id.add_to_favorites_item && feedItem != null -> {
|
||||
DBWriter.addFavoriteItem(feedItem)
|
||||
isFavorite = true
|
||||
videoEpisodeFragment.isFavorite = true
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
item.itemId == R.id.remove_from_favorites_item && feedItem != null -> {
|
||||
DBWriter.removeFavoriteItem(feedItem)
|
||||
isFavorite = false
|
||||
videoEpisodeFragment.isFavorite = false
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
item.itemId == R.id.disable_sleeptimer_item || item.itemId == R.id.set_sleeptimer_item -> {
|
||||
|
@ -615,93 +314,10 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
fun onPositionObserverUpdate() {
|
||||
if (controller == null) return
|
||||
|
||||
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
|
||||
val currentPosition = converter.convert(controller!!.position)
|
||||
val duration = converter.convert(controller!!.duration)
|
||||
val remainingTime = converter.convert(
|
||||
controller!!.duration - controller!!.position)
|
||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||
if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) {
|
||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||
return
|
||||
}
|
||||
binding.positionLabel.text = getDurationStringLong(currentPosition)
|
||||
if (showTimeLeft) {
|
||||
binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
|
||||
} else {
|
||||
binding.durationLabel.text = getDurationStringLong(duration)
|
||||
}
|
||||
updateProgressbarPosition(currentPosition, duration)
|
||||
}
|
||||
|
||||
private fun updateProgressbarPosition(position: Int, duration: Int) {
|
||||
Log.d(TAG, "updateProgressbarPosition($position, $duration)")
|
||||
val progress = (position.toFloat()) / duration
|
||||
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (controller == null) return
|
||||
|
||||
if (fromUser) {
|
||||
prog = progress / (seekBar.max.toFloat())
|
||||
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
|
||||
val position = converter.convert((prog * controller!!.duration).toInt())
|
||||
binding.seekPositionLabel.text = getDurationStringLong(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
binding.seekCardView.scaleX = .8f
|
||||
binding.seekCardView.scaleY = .8f
|
||||
binding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
if (controller != null) {
|
||||
controller!!.seekTo((prog * controller!!.duration).toInt())
|
||||
}
|
||||
binding.seekCardView.scaleX = 1f
|
||||
binding.seekCardView.scaleY = 1f
|
||||
binding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
private fun checkFavorite() {
|
||||
val feedItem = getFeedItem(controller?.getMedia()) ?: return
|
||||
disposable?.dispose()
|
||||
|
||||
disposable = Observable.fromCallable { DBReader.getFeedItem(feedItem.id) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ item: FeedItem? ->
|
||||
if (item != null) {
|
||||
val isFav = item.isTagged(FeedItem.TAG_FAVORITE)
|
||||
if (isFavorite != isFav) {
|
||||
isFavorite = isFav
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
}
|
||||
|
||||
private fun compatEnterPictureInPicture() {
|
||||
if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(false)
|
||||
if (videoMode == FULL_SCREEN_VIEW) supportActionBar?.hide()
|
||||
videoEpisodeFragment.hideVideoControls(false)
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
@ -715,18 +331,18 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE -> {
|
||||
onPlayPause()
|
||||
toggleVideoControlsVisibility()
|
||||
videoEpisodeFragment.onPlayPause()
|
||||
videoEpisodeFragment.toggleVideoControlsVisibility()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> {
|
||||
onRewind()
|
||||
showSkipAnimation(false)
|
||||
videoEpisodeFragment.onRewind()
|
||||
videoEpisodeFragment.showSkipAnimation(false)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> {
|
||||
onFastForward()
|
||||
showSkipAnimation(true)
|
||||
videoEpisodeFragment.onFastForward()
|
||||
videoEpisodeFragment.showSkipAnimation(true)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_ESCAPE -> {
|
||||
|
@ -756,7 +372,8 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
}
|
||||
//Go to x% of video:
|
||||
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
|
||||
controller?.seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller!!.duration).toInt())
|
||||
val controller = videoEpisodeFragment.controller
|
||||
controller?.seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller.duration).toInt())
|
||||
return true
|
||||
}
|
||||
return super.onKeyUp(keyCode, event)
|
||||
|
@ -764,23 +381,26 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
|||
|
||||
companion object {
|
||||
private const val TAG = "VideoplayerActivity"
|
||||
const val WINDOW_VIEW = 1
|
||||
const val FULL_SCREEN_VIEW = 2
|
||||
const val AUDIO_ONLY = 3
|
||||
|
||||
private fun getWebsiteLinkWithFallback(media: Playable?): String? {
|
||||
when {
|
||||
return when {
|
||||
media == null -> {
|
||||
return null
|
||||
null
|
||||
}
|
||||
!media.getWebsiteLink().isNullOrBlank() -> {
|
||||
return media.getWebsiteLink()
|
||||
media.getWebsiteLink()
|
||||
}
|
||||
media is FeedMedia -> {
|
||||
return getLinkWithFallback(media.item)
|
||||
getLinkWithFallback(media.item)
|
||||
}
|
||||
else -> return null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFeedItem(playable: Playable?): FeedItem? {
|
||||
fun getFeedItem(playable: Playable?): FeedItem? {
|
||||
return if (playable is FeedMedia) {
|
||||
playable.item
|
||||
} else {
|
||||
|
|
|
@ -104,9 +104,6 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
|||
binding.global.isChecked = true
|
||||
}
|
||||
}
|
||||
// if (!settingCode[0]) binding.currentAudio.visibility = View.INVISIBLE
|
||||
// if (!settingCode[1]) binding.currentPodcast.visibility = View.INVISIBLE
|
||||
// if (!settingCode[2]) binding.global.visibility = View.INVISIBLE
|
||||
|
||||
speedSeekBar = binding.speedSeekBar
|
||||
speedSeekBar.setProgressChangedListener { multiplier: Float ->
|
||||
|
@ -184,9 +181,11 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
|||
true
|
||||
}
|
||||
holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (binding.currentAudio.isChecked) settingCode[0] = true
|
||||
if (binding.currentPodcast.isChecked) settingCode[1] = true
|
||||
if (binding.global.isChecked) settingCode[2] = true
|
||||
Log.d("VariableSpeedDialog", "holder.chip settingCode0: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
|
||||
settingCode[0] = binding.currentAudio.isChecked
|
||||
settingCode[1] = binding.currentPodcast.isChecked
|
||||
settingCode[2] = binding.global.isChecked
|
||||
Log.d("VariableSpeedDialog", "holder.chip settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
|
||||
|
||||
if (controller != null) {
|
||||
dismiss()
|
||||
|
@ -203,13 +202,17 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
|||
return selectedSpeeds[position].hashCode().toLong()
|
||||
}
|
||||
|
||||
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(
|
||||
chip)
|
||||
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* @param settingCode_ array at input indicate which categories can be set, at output which categories are changed
|
||||
* @param index_default indicates which category is checked by default
|
||||
*/
|
||||
fun newInstance(settingCode_: BooleanArray? = null, index_default: Int? = null): VariableSpeedDialog? {
|
||||
val settingCode = settingCode_ ?: BooleanArray(3){false}
|
||||
val settingCode = settingCode_ ?: BooleanArray(3){true}
|
||||
Log.d("VariableSpeedDialog", "newInstance settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
|
||||
if (settingCode.size != 3) {
|
||||
Log.e("VariableSpeedDialog", "wrong settingCode dimension")
|
||||
return null
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -6,11 +6,14 @@ import ac.mdiq.podcini.databinding.InternalPlayerFragmentBinding
|
|||
import ac.mdiq.podcini.feed.util.ImageResourceUtils
|
||||
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
|
||||
import ac.mdiq.podcini.playback.PlaybackController
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.storage.model.feed.Chapter
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedMedia
|
||||
|
@ -23,6 +26,7 @@ import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
|||
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.AUDIO_ONLY
|
||||
import ac.mdiq.podcini.ui.view.ChapterSeekBar
|
||||
import ac.mdiq.podcini.ui.view.PlayButton
|
||||
import ac.mdiq.podcini.util.ChapterUtils
|
||||
|
@ -97,7 +101,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
private var currentMedia: Playable? = null
|
||||
private var currentitem: FeedItem? = null
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
|
@ -447,6 +450,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun scrollToTop() {
|
||||
itemDescFrag.scrollToTop()
|
||||
}
|
||||
|
||||
fun fadePlayerToToolbar(slideOffset: Float) {
|
||||
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat()
|
||||
val player = playerView1
|
||||
|
@ -515,9 +523,12 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
Log.d(TAG, "internalPlayerFragment was clicked")
|
||||
val media = controller?.getMedia()
|
||||
if (media != null) {
|
||||
if (media.getMediaType() == MediaType.AUDIO) {
|
||||
if (media.getMediaType() == MediaType.AUDIO || videoPlayMode == AUDIO_ONLY) {
|
||||
controller!!.ensureService()
|
||||
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
|
||||
} else {
|
||||
controller?.playPause()
|
||||
// controller!!.ensureService()
|
||||
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
@ -712,19 +723,17 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
.fitCenter()
|
||||
.dontAnimate()
|
||||
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media) + "sdfsdf"
|
||||
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
|
||||
when {
|
||||
!imgLoc.isNullOrBlank() -> Glide.with(this)
|
||||
|
||||
Glide.with(this)
|
||||
.load(imgLoc)
|
||||
.apply(options)
|
||||
.into(imgvCover)
|
||||
!imgLocFB.isNullOrBlank() -> Glide.with(this)
|
||||
.error(Glide.with(this)
|
||||
.load(imgLocFB)
|
||||
.error(R.mipmap.ic_launcher)
|
||||
.apply(options))
|
||||
.apply(options)
|
||||
.into(imgvCover)
|
||||
else -> imgvCover.setImageResource(R.mipmap.ic_launcher)
|
||||
}
|
||||
|
||||
if (controller?.isPlayingVideoLocally == true) {
|
||||
(activity as MainActivity).bottomSheet.setLocked(true)
|
||||
|
@ -747,7 +756,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val TAG: String = "AudioPlayerFragment"
|
||||
}
|
||||
|
|
|
@ -316,7 +316,6 @@ class PlayerDetailsFragment : Fragment() {
|
|||
|
||||
@UnstableApi private fun seekToPrevChapter() {
|
||||
val curr: Chapter? = currentChapter
|
||||
|
||||
if (controller == null || curr == null || displayedChapterIndex == -1) return
|
||||
|
||||
when {
|
||||
|
@ -353,8 +352,8 @@ class PlayerDetailsFragment : Fragment() {
|
|||
val prefs = requireActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE)
|
||||
val editor = prefs.edit()
|
||||
if (controller?.getMedia() != null) {
|
||||
Log.d(TAG, "Saving scroll position: " + webvDescription.scrollY)
|
||||
editor.putInt(PREF_SCROLL_Y, webvDescription.scrollY)
|
||||
Log.d(TAG, "Saving scroll position: " + binding.itemDescriptionFragment.scrollY)
|
||||
editor.putInt(PREF_SCROLL_Y, binding.itemDescriptionFragment.scrollY)
|
||||
editor.putString(PREF_PLAYABLE_ID, controller!!.getMedia()!!.getIdentifier().toString())
|
||||
} else {
|
||||
Log.d(TAG, "savePreferences was called while media or webview was null")
|
||||
|
@ -374,11 +373,12 @@ class PlayerDetailsFragment : Fragment() {
|
|||
if (scrollY != -1) {
|
||||
if (id == controller?.getMedia()?.getIdentifier()?.toString()) {
|
||||
Log.d(TAG, "Restored scroll Position: $scrollY")
|
||||
webvDescription.scrollTo(webvDescription.scrollX, scrollY)
|
||||
binding.itemDescriptionFragment.scrollTo(binding.itemDescriptionFragment.scrollX, scrollY)
|
||||
return true
|
||||
}
|
||||
Log.d(TAG, "reset scroll Position: 0")
|
||||
webvDescription.scrollTo(webvDescription.scrollX, 0)
|
||||
binding.itemDescriptionFragment.scrollTo(0, 0)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -386,7 +386,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
|
||||
fun scrollToTop() {
|
||||
webvDescription.scrollTo(0, 0)
|
||||
binding.itemDescriptionFragment.scrollTo(0, 0)
|
||||
savePreference()
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -2,13 +2,14 @@ package ac.mdiq.podcini.ui.view
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.widget.VideoView
|
||||
import kotlin.math.ceil
|
||||
|
||||
class AspectRatioVideoView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0
|
||||
) : VideoView(context, attrs, defStyle) {
|
||||
defStyle: Int = 0)
|
||||
: VideoView(context, attrs, defStyle) {
|
||||
|
||||
private var mVideoWidth = 0
|
||||
private var mVideoHeight = 0
|
||||
|
@ -20,7 +21,7 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
|
|||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "onMeasure $mAvailableWidth $mAvailableHeight")
|
||||
if (mAvailableWidth < 0 || mAvailableHeight < 0) {
|
||||
mAvailableWidth = width.toFloat()
|
||||
mAvailableHeight = height.toFloat()
|
||||
|
@ -54,6 +55,7 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
|
|||
// Set the new video size
|
||||
mVideoWidth = videoWidth
|
||||
mVideoHeight = videoHeight
|
||||
Log.d(TAG, "setVideoSize $mVideoWidth $mVideoHeight")
|
||||
|
||||
/*
|
||||
* If this isn't set the video is stretched across the
|
||||
|
@ -64,8 +66,8 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
|
|||
*/
|
||||
holder.setFixedSize(videoWidth, videoHeight)
|
||||
|
||||
requestLayout()
|
||||
invalidate()
|
||||
// requestLayout()
|
||||
// invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,6 +78,11 @@ class AspectRatioVideoView @JvmOverloads constructor(context: Context,
|
|||
fun setAvailableSize(width: Float, height: Float) {
|
||||
mAvailableWidth = width
|
||||
mAvailableHeight = height
|
||||
requestLayout()
|
||||
Log.d(TAG, "setAvailableSize $mAvailableWidth $mAvailableHeight")
|
||||
// requestLayout()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AspectRatioVideoView"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ object WidgetUpdater {
|
|||
val views = RemoteViews(context.packageName, R.layout.player_widget)
|
||||
|
||||
if (widgetState.media != null) {
|
||||
val icon: Bitmap?
|
||||
var icon: Bitmap? = null
|
||||
val iconSize = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer)
|
||||
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer)
|
||||
|
@ -65,26 +65,21 @@ object WidgetUpdater {
|
|||
.transform(FitCenter(), RoundedCorners(radius))
|
||||
|
||||
try {
|
||||
var imgLoc = widgetState.media.getImageLocation()
|
||||
if (imgLoc != null) {
|
||||
val imgLoc = widgetState.media.getImageLocation()
|
||||
val imgLoc1 = getFallbackImageLocation(widgetState.media)
|
||||
icon = Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgLoc)
|
||||
.error(Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgLoc1)
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)[500, TimeUnit.MILLISECONDS])
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)
|
||||
.get(500, TimeUnit.MILLISECONDS)
|
||||
views.setImageViewBitmap(R.id.imgvCover, icon)
|
||||
} else {
|
||||
imgLoc = getFallbackImageLocation(widgetState.media)
|
||||
if (imgLoc != null) {
|
||||
icon = Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgLoc)
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)[500, TimeUnit.MILLISECONDS]
|
||||
views.setImageViewBitmap(R.id.imgvCover, icon)
|
||||
} else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher)
|
||||
}
|
||||
if (icon != null) views.setImageViewBitmap(R.id.imgvCover, icon)
|
||||
else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher)
|
||||
} catch (tr1: Throwable) {
|
||||
Log.e(TAG, "Error loading the media icon for the widget", tr1)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -5,158 +5,15 @@
|
|||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/videoPlayerContainer">
|
||||
android:id="@+id/videoPlayerContainer"
|
||||
android:windowActionBarOverlay="true">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.AspectRatioVideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminateOnly="true"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/controlsContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layoutDirection="ltr"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/rewindButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/rewind_label"
|
||||
app:srcCompat="@drawable/ic_fast_rewind_video_white" />
|
||||
|
||||
<ac.mdiq.podcini.ui.view.PlayButton
|
||||
android:id="@+id/playButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pause_label"
|
||||
app:srcCompat="@drawable/ic_pause_video_white" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/fastForwardButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/fast_forward_label"
|
||||
app:srcCompat="@drawable/ic_fast_forward_video_white" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/skipAnimationImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:padding="64dp"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottomControlsContainer"
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/main_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/seekCardView"
|
||||
android:alpha="0"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_gravity="center"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardBackgroundColor="?attr/seek_background"
|
||||
app:cardElevation="0dp"
|
||||
tools:alpha="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/seekPositionLabel"
|
||||
android:gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
tools:text="1:06:29" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:background="#80000000"
|
||||
android:layoutDirection="ltr"
|
||||
android:paddingTop="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/positionLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/position_default_label"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/durationLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/position_default_label"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/sbPosition"
|
||||
android:layout_width="0px"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@+id/durationLabel"
|
||||
android:layout_toStartOf="@+id/durationLabel"
|
||||
android:layout_toRightOf="@+id/positionLabel"
|
||||
android:layout_toEndOf="@+id/positionLabel"
|
||||
android:layout_centerInParent="true"
|
||||
android:max="500" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?android:windowContentOverlay"
|
||||
tools:background="@android:color/holo_red_dark"
|
||||
android:windowActionBarOverlay="true"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
|
@ -177,6 +177,17 @@
|
|||
<item>@string/add_feed_label</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="video_mode_options">
|
||||
<item>@string/pref_video_mode_small_window</item>
|
||||
<item>@string/pref_video_mode_full_screen</item>
|
||||
<item>@string/pref_video_mode_audio_only</item>
|
||||
</string-array>
|
||||
<string-array name="video_mode_options_values">
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="nav_drawer_feed_order_options">
|
||||
<item>@string/drawer_feed_order_unplayed_episodes</item>
|
||||
<item>@string/drawer_feed_order_alphabetical</item>
|
||||
|
|
|
@ -487,6 +487,11 @@
|
|||
<string name="pref_fast_forward_sum">Customize the number of seconds to jump forward when the fast forward button is clicked</string>
|
||||
<string name="pref_rewind">Rewind skip time</string>
|
||||
<string name="pref_rewind_sum">Customize the number of seconds to jump backwards when the rewind button is clicked</string>
|
||||
<string name="pref_playback_video_mode">Video play mode</string>
|
||||
<string name="pref_playback_video_mode_sum">Choose how video episode is played by default</string>
|
||||
<string name="pref_video_mode_full_screen">Full screen</string>
|
||||
<string name="pref_video_mode_small_window">Small window</string>
|
||||
<string name="pref_video_mode_audio_only">Audio only</string>
|
||||
<string name="pref_expandNotify_title">High notification priority</string>
|
||||
<string name="pref_expandNotify_sum">This usually expands the notification to show playback buttons.</string>
|
||||
<string name="pref_persistNotify_title">Persistent playback controls</string>
|
||||
|
@ -688,6 +693,7 @@
|
|||
<string name="toolbar_back_button_content_description">Back</string>
|
||||
<string name="rewind_label">Rewind</string>
|
||||
<string name="fast_forward_label">Fast forward</string>
|
||||
<string name="toggle_video_views">Toggle video views</string>
|
||||
<string name="increase_speed">Increase speed</string>
|
||||
<string name="decrease_speed">Decrease speed</string>
|
||||
<string name="media_type_video_label">Video</string>
|
||||
|
|
|
@ -227,6 +227,16 @@
|
|||
<item name="windowSplashScreenAnimatedIcon">@drawable/launcher_animate</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Podcini.VideoEpisode" parent="@style/Theme.Podcini.Dark">
|
||||
<item name="android:fitsSystemWindows">true</item>
|
||||
<!-- <item name="android:windowActionModeOverlay">true</item>-->
|
||||
<!-- <item name="android:windowActionBarOverlay">true</item>-->
|
||||
<!-- <item name="android:windowActionBar">true</item>-->
|
||||
<!-- <item name="windowActionModeOverlay">true</item>-->
|
||||
<!-- <item name="windowActionBar">true</item>-->
|
||||
<item name="windowActionBarOverlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
|
||||
<item name="windowActionBarOverlay">true</item>
|
||||
</style>
|
||||
|
|
|
@ -61,6 +61,11 @@
|
|||
android:key="prefStreamOverDownload"
|
||||
android:summary="@string/pref_stream_over_download_sum"
|
||||
android:title="@string/pref_stream_over_download_title"/>
|
||||
<Preference
|
||||
android:title="@string/pref_playback_video_mode"
|
||||
android:key="prefPlaybackVideoModeLauncher"
|
||||
android:summary="@string/pref_playback_video_mode_sum"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/reassign_hardware_buttons">
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
android:title="@string/pref_nav_drawer_feed_counter_title"
|
||||
android:key="prefDrawerFeedIndicator"
|
||||
android:summary="@string/pref_nav_drawer_feed_counter_sum"
|
||||
android:defaultValue="1"/>
|
||||
android:defaultValue="2"/>
|
||||
<!-- <Preference-->
|
||||
<!-- android:title="@string/pref_filter_feed_title"-->
|
||||
<!-- android:key="prefSubscriptionsFilter"-->
|
||||
|
|
12
changelog.md
12
changelog.md
|
@ -261,3 +261,15 @@
|
|||
* fixed bug of receiving null view in function requiring non-null in subscriptions page
|
||||
* set Counter default to number of SHOW_UNPLAYED
|
||||
* removed FeedCounter.SHOW_NEW, NewEpisodesNotification, and associated notifications settings
|
||||
|
||||
## 4.8.0
|
||||
|
||||
* fixed empty player detailed view on first start
|
||||
* player detailed view scrolls to the top on a new episode
|
||||
* created video episode view, with video player on top and episode descriptions in portrait mode
|
||||
* added video player mode setting in preferences, set to small window by default
|
||||
* added on video controller easy switches to other video mode or audio only
|
||||
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
|
||||
* webkit updated to Androidx
|
||||
* fixed bug in setting speed to wrong categories
|
||||
* improved fetching of episode images when invalid addresses are given
|
|
@ -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
|
Loading…
Reference in New Issue