diff --git a/README.md b/README.md index 4bfad593..04461830 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index f09c6907..37b7b5c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2b9ef2e..39b4ccdc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -246,11 +246,12 @@ android:value="ac.mdiq.podcini.ui.activity.PreferenceActivity"/> + + { - 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) - .asBitmap() - .load(imgLoc) - .apply(options) - .submit(iconSize, iconSize) - .get() - } - } - } + val imgLoc = playable?.getImageLocation() + val imgLoc1 = ImageResourceUtils.getFallbackImageLocation(playable!!) + Log.d(TAG, "loadIcon imgLoc $imgLoc $imgLoc1") + cachedIcon = Glide.with(context) + .asBitmap() + .load(imgLoc) + .error(Glide.with(context) + .asBitmap() + .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) { diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt index af830e41..1190e4f6 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -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?) { prefs.edit() diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt index d9ef558c..8122a21c 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt @@ -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(PREF_PLAYBACK_VIDEO_MODE_LAUNCHER)?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + VideoModeDialog.showDialog(requireContext()) + true + } + findPreference(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" } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt index e5600a95..b62ad692 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -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) } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/PlaybackSpeedDialogActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/PlaybackSpeedDialogActivity.kt index 661bc743..d3a1fa57 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/PlaybackSpeedDialogActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/PlaybackSpeedDialogActivity.kt @@ -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) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt index d25ce3ad..5d98816a 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt @@ -113,15 +113,13 @@ class SelectSubscriptionActivity : AppCompatActivity() { .apply(RequestOptions.overrideOf(iconSize, iconSize)) .listener(object : RequestListener { @UnstableApi override fun onLoadFailed(e: GlideException?, model: Any?, - target: Target, isFirstResource: Boolean - ): Boolean { + target: Target, isFirstResource: Boolean): Boolean { addShortcut(feed, null) return true } @UnstableApi override fun onResourceReady(resource: Bitmap, model: Any, - target: Target, dataSource: DataSource, isFirstResource: Boolean - ): Boolean { + target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean { addShortcut(feed, resource) return true } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index 959aa21e..46358742 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -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) - 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) + + 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() { - 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() - } - } + fun toggleViews() { + val newIntent = Intent(this, VideoplayerActivity::class.java) + newIntent.putExtra("fullScreenMode", if (videoMode == FULL_SCREEN_VIEW) WINDOW_VIEW else FULL_SCREEN_VIEW) + finish() + 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 { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index f66b9b6f..8aaee9ef 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -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 diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VideoModeDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VideoModeDialog.kt new file mode 100644 index 00000000..c69ac5f6 --- /dev/null +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VideoModeDialog.kt @@ -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() + } +} diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index f4061f25..4da2f77d 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -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) - .load(imgLoc) - .apply(options) - .into(imgvCover) - !imgLocFB.isNullOrBlank() -> Glide.with(this) + + Glide.with(this) + .load(imgLoc) + .error(Glide.with(this) .load(imgLocFB) - .apply(options) - .into(imgvCover) - else -> imgvCover.setImageResource(R.mipmap.ic_launcher) - } + .error(R.mipmap.ic_launcher) + .apply(options)) + .apply(options) + .into(imgvCover) 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" } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index 07864010..b2120f94 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -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() } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt new file mode 100644 index 00000000..43dfb88b --- /dev/null +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt @@ -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 { 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" + } +} \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/ui/view/AspectRatioVideoView.kt b/app/src/main/java/ac/mdiq/podcini/ui/view/AspectRatioVideoView.kt index cd07e0d0..bcaa975f 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/view/AspectRatioVideoView.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/view/AspectRatioVideoView.kt @@ -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" } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt index b881bae5..1e5a61a3 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt @@ -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) { - icon = Glide.with(context) + val imgLoc = widgetState.media.getImageLocation() + val imgLoc1 = getFallbackImageLocation(widgetState.media) + icon = Glide.with(context) + .asBitmap() + .load(imgLoc) + .error(Glide.with(context) .asBitmap() - .load(imgLoc) + .load(imgLoc1) .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) - } + .submit(iconSize, iconSize)[500, TimeUnit.MILLISECONDS]) + .apply(options) + .submit(iconSize, iconSize) + .get(500, TimeUnit.MILLISECONDS) + 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) } diff --git a/app/src/main/res/drawable/baseline_fullscreen_24.xml b/app/src/main/res/drawable/baseline_fullscreen_24.xml new file mode 100644 index 00000000..c19ee7b2 --- /dev/null +++ b/app/src/main/res/drawable/baseline_fullscreen_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/video_episode_fragment.xml b/app/src/main/res/layout/video_episode_fragment.xml new file mode 100644 index 00000000..1039ef0d --- /dev/null +++ b/app/src/main/res/layout/video_episode_fragment.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/videoplayer_activity.xml b/app/src/main/res/layout/videoplayer_activity.xml index 43c1c6bf..ff180ef9 100644 --- a/app/src/main/res/layout/videoplayer_activity.xml +++ b/app/src/main/res/layout/videoplayer_activity.xml @@ -1,162 +1,19 @@ + 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/videoPlayerContainer" + android:windowActionBarOverlay="true"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:foreground="?android:windowContentOverlay" + tools:background="@android:color/holo_red_dark" + android:windowActionBarOverlay="true"/> diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png deleted file mode 100644 index ae9ba36e..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 104811f7..1c6d8470 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -177,6 +177,17 @@ @string/add_feed_label + + @string/pref_video_mode_small_window + @string/pref_video_mode_full_screen + @string/pref_video_mode_audio_only + + + 1 + 2 + 3 + + @string/drawer_feed_order_unplayed_episodes @string/drawer_feed_order_alphabetical diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6d593c1..9ae6fc9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -487,6 +487,11 @@ Customize the number of seconds to jump forward when the fast forward button is clicked Rewind skip time Customize the number of seconds to jump backwards when the rewind button is clicked + Video play mode + Choose how video episode is played by default + Full screen + Small window + Audio only High notification priority This usually expands the notification to show playback buttons. Persistent playback controls @@ -688,6 +693,7 @@ Back Rewind Fast forward + Toggle video views Increase speed Decrease speed Video diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 329f306c..b672b111 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -227,6 +227,16 @@ @drawable/launcher_animate + + diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml index 4b98baae..4ed2b16f 100644 --- a/app/src/main/res/xml/preferences_playback.xml +++ b/app/src/main/res/xml/preferences_playback.xml @@ -61,6 +61,11 @@ android:key="prefStreamOverDownload" android:summary="@string/pref_stream_over_download_sum" android:title="@string/pref_stream_over_download_title"/> + + diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml index b6afa89a..bc1b1429 100644 --- a/app/src/main/res/xml/preferences_user_interface.xml +++ b/app/src/main/res/xml/preferences_user_interface.xml @@ -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"/> diff --git a/changelog.md b/changelog.md index 7c1fce41..d79cbca4 100644 --- a/changelog.md +++ b/changelog.md @@ -260,4 +260,16 @@ * enabled speed setting for podcast in video player * 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 \ No newline at end of file +* 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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/3020130.txt b/fastlane/metadata/android/en-US/changelogs/3020130.txt new file mode 100644 index 00000000..d9667c54 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020130.txt @@ -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