6.5.9 commit
This commit is contained in:
parent
e3f7c31407
commit
fb114f349b
|
@ -16,7 +16,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
||||||
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
||||||
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
|
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
|
||||||
|
|
||||||
This project evolves from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
|
This project is developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
|
||||||
|
|
||||||
Compared to AntennaPod this project:
|
Compared to AntennaPod this project:
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ android {
|
||||||
testApplicationId "ac.mdiq.podcini.tests"
|
testApplicationId "ac.mdiq.podcini.tests"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
versionCode 3020242
|
versionCode 3020243
|
||||||
versionName "6.5.8"
|
versionName "6.5.9"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
|
|
@ -54,6 +54,7 @@ import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||||
|
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||||
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
||||||
|
@ -738,13 +739,10 @@ class PlaybackService : MediaLibraryService() {
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
Logd(TAG, "onTaskRemoved")
|
Logd(TAG, "onTaskRemoved")
|
||||||
val player = mediaSession?.player
|
val player = mediaSession?.player ?: return
|
||||||
if (player != null) {
|
if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == STATE_ENDED) {
|
||||||
if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == STATE_ENDED) {
|
// Stop the service if not playing, continue playing in the background otherwise.
|
||||||
// Stop the service if not playing, continue playing in the background
|
stopSelf()
|
||||||
// otherwise.
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2569,7 +2567,9 @@ class PlaybackService : MediaLibraryService() {
|
||||||
playbackService?.mPlayer?.prepare()
|
playbackService?.mPlayer?.prepare()
|
||||||
playbackService?.taskManager?.restartSleepTimer()
|
playbackService?.taskManager?.restartSleepTimer()
|
||||||
}
|
}
|
||||||
else -> Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown")
|
else -> {
|
||||||
|
Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
||||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
|
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||||
import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme
|
import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer
|
import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer
|
||||||
|
@ -36,6 +37,7 @@ import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -64,6 +66,8 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.session.MediaController
|
||||||
|
import androidx.media3.session.SessionToken
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
@ -72,6 +76,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
|
@ -93,6 +99,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
private lateinit var audioPlayerView: View
|
private lateinit var audioPlayerView: View
|
||||||
private lateinit var navDrawer: View
|
private lateinit var navDrawer: View
|
||||||
private lateinit var dummyView : View
|
private lateinit var dummyView : View
|
||||||
|
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||||
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
@ -174,7 +181,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
buildTags()
|
buildTags()
|
||||||
monitorFeeds()
|
monitorFeeds()
|
||||||
// InTheatre.apply { }
|
// InTheatre.apply { }
|
||||||
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
AudioPlayerFragment.PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
||||||
PlayerWidget.getSharedPrefs(this@MainActivity)
|
PlayerWidget.getSharedPrefs(this@MainActivity)
|
||||||
StatisticsFragment.getSharedPrefs(this@MainActivity)
|
StatisticsFragment.getSharedPrefs(this@MainActivity)
|
||||||
OnlineFeedFragment.getSharedPrefs(this@MainActivity)
|
OnlineFeedFragment.getSharedPrefs(this@MainActivity)
|
||||||
|
@ -189,13 +196,10 @@ class MainActivity : CastEnabledActivity() {
|
||||||
// setContentView(R.layout.main_activity)
|
// setContentView(R.layout.main_activity)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25)
|
recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25)
|
||||||
|
|
||||||
dummyView = object : View(this) {}
|
dummyView = object : View(this) {}
|
||||||
|
|
||||||
drawerLayout = findViewById(R.id.main_layout)
|
drawerLayout = findViewById(R.id.main_layout)
|
||||||
navDrawer = findViewById(R.id.navDrawerFragment)
|
navDrawer = findViewById(R.id.navDrawerFragment)
|
||||||
setNavDrawerSize()
|
setNavDrawerSize()
|
||||||
|
|
||||||
mainView = findViewById(R.id.main_view)
|
mainView = findViewById(R.id.main_view)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
@ -208,9 +212,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(mainView) { _: View?, insets: WindowInsetsCompat ->
|
ViewCompat.setOnApplyWindowInsetsListener(mainView) { _: View?, insets: WindowInsetsCompat ->
|
||||||
navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
updateInsets()
|
updateInsets()
|
||||||
WindowInsetsCompat.Builder(insets)
|
WindowInsetsCompat.Builder(insets).setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE).build()
|
||||||
.setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
|
@ -220,14 +222,9 @@ class MainActivity : CastEnabledActivity() {
|
||||||
val lastFragment = NavDrawerFragment.getLastNavFragment()
|
val lastFragment = NavDrawerFragment.getLastNavFragment()
|
||||||
if (ArrayUtils.contains(NavDrawerFragment.NAV_DRAWER_TAGS, lastFragment)) loadFragment(lastFragment, null)
|
if (ArrayUtils.contains(NavDrawerFragment.NAV_DRAWER_TAGS, lastFragment)) loadFragment(lastFragment, null)
|
||||||
else {
|
else {
|
||||||
try {
|
// it's not a number, this happens if we removed a label from the NAV_DRAWER_TAGS give them a nice default...
|
||||||
loadFeedFragmentById(lastFragment.toInt().toLong(), null)
|
try { loadFeedFragmentById(lastFragment.toInt().toLong(), null) }
|
||||||
} catch (e: NumberFormatException) {
|
catch (e: NumberFormatException) { loadFragment(SubscriptionsFragment.TAG, null) }
|
||||||
// it's not a number, this happens if we removed
|
|
||||||
// a label from the NAV_DRAWER_TAGS
|
|
||||||
// give them a nice default...
|
|
||||||
loadFragment(SubscriptionsFragment.TAG, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -367,6 +364,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
_binding = null
|
_binding = null
|
||||||
// realm.close()
|
// realm.close()
|
||||||
drawerLayout?.removeDrawerListener(drawerToggle!!)
|
drawerLayout?.removeDrawerListener(drawerToggle!!)
|
||||||
|
MediaController.releaseFuture(controllerFuture)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,14 +465,9 @@ class MainActivity : CastEnabledActivity() {
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun loadChildFragment(fragment: Fragment, transition: TransitionEffect? = TransitionEffect.NONE) {
|
fun loadChildFragment(fragment: Fragment, transition: TransitionEffect? = TransitionEffect.NONE) {
|
||||||
val transaction = supportFragmentManager.beginTransaction()
|
val transaction = supportFragmentManager.beginTransaction()
|
||||||
|
|
||||||
when (transition) {
|
when (transition) {
|
||||||
TransitionEffect.FADE -> transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
|
TransitionEffect.FADE -> transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
|
||||||
TransitionEffect.SLIDE -> transaction.setCustomAnimations(
|
TransitionEffect.SLIDE -> transaction.setCustomAnimations(R.anim.slide_right_in, R.anim.slide_left_out, R.anim.slide_left_in, R.anim.slide_right_out)
|
||||||
R.anim.slide_right_in,
|
|
||||||
R.anim.slide_left_out,
|
|
||||||
R.anim.slide_left_in,
|
|
||||||
R.anim.slide_right_out)
|
|
||||||
TransitionEffect.NONE -> {}
|
TransitionEffect.NONE -> {}
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
|
@ -518,6 +511,12 @@ class MainActivity : CastEnabledActivity() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
RatingDialog.init(this)
|
RatingDialog.init(this)
|
||||||
|
val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||||
|
controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
||||||
|
controllerFuture.addListener({
|
||||||
|
// mediaController = controllerFuture.get()
|
||||||
|
// Logd(TAG, "controllerFuture.addListener: $mediaController")
|
||||||
|
}, MoreExecutors.directExecutor())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
@ -628,7 +627,6 @@ class MainActivity : CastEnabledActivity() {
|
||||||
val tag = intent.getStringExtra(MainActivityStarter.Extras.fragment_tag.name)
|
val tag = intent.getStringExtra(MainActivityStarter.Extras.fragment_tag.name)
|
||||||
val args = intent.getBundleExtra(MainActivityStarter.Extras.fragment_args.name)
|
val args = intent.getBundleExtra(MainActivityStarter.Extras.fragment_args.name)
|
||||||
if (tag != null) loadFragment(tag, args)
|
if (tag != null) loadFragment(tag, args)
|
||||||
|
|
||||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||||
}
|
}
|
||||||
intent.getBooleanExtra(MainActivityStarter.Extras.open_player.name, false) -> {
|
intent.getBooleanExtra(MainActivityStarter.Extras.open_player.name, false) -> {
|
||||||
|
|
|
@ -2,7 +2,9 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
|
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
|
||||||
|
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
|
||||||
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
|
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
|
||||||
|
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
||||||
|
@ -12,24 +14,27 @@ import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybac
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.playback.base.VideoMode
|
import ac.mdiq.podcini.playback.base.VideoMode
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.toggleFallbackSpeed
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.toggleFallbackSpeed
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||||
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.model.*
|
import ac.mdiq.podcini.storage.model.*
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
||||||
|
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
|
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||||
|
@ -38,46 +43,58 @@ import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||||
|
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||||
import ac.mdiq.podcini.ui.view.ChapterSeekBar
|
import ac.mdiq.podcini.ui.view.ChapterSeekBar
|
||||||
import ac.mdiq.podcini.ui.view.PlayButton
|
import ac.mdiq.podcini.ui.view.PlayButton
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
import ac.mdiq.podcini.util.MiscFormatter
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ComponentName
|
import android.content.*
|
||||||
import android.content.Intent
|
import android.graphics.ColorFilter
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import android.view.View.OnLayoutChangeListener
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ErrorResult
|
import coil.request.ErrorResult
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.elevation.SurfaceColors
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.dankito.readability4j.Readability4J
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
@ -171,6 +188,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
_binding = null
|
_binding = null
|
||||||
controller?.release()
|
controller?.release()
|
||||||
controller = null
|
controller = null
|
||||||
|
// MediaController.releaseFuture(controllerFuture)
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,12 +311,12 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
super.onStart()
|
super.onStart()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
|
|
||||||
val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
|
// val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
|
||||||
controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
|
// controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
|
||||||
controllerFuture.addListener({
|
// controllerFuture.addListener({
|
||||||
// mediaController = controllerFuture.get()
|
//// mediaController = controllerFuture.get()
|
||||||
// Logd(TAG, "controllerFuture.addListener: $mediaController")
|
//// Logd(TAG, "controllerFuture.addListener: $mediaController")
|
||||||
}, MoreExecutors.directExecutor())
|
// }, MoreExecutors.directExecutor())
|
||||||
|
|
||||||
loadMediaInfo(false)
|
loadMediaInfo(false)
|
||||||
}
|
}
|
||||||
|
@ -306,7 +324,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
Logd(TAG, "onStop()")
|
Logd(TAG, "onStop()")
|
||||||
super.onStop()
|
super.onStop()
|
||||||
MediaController.releaseFuture(controllerFuture)
|
// MediaController.releaseFuture(controllerFuture)
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
|
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
|
||||||
}
|
}
|
||||||
|
@ -449,7 +467,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
|
EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
|
||||||
|
|
||||||
val mediaType = curMedia?.getMediaType()
|
val mediaType = curMedia?.getMediaType()
|
||||||
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO)
|
val notAudioOnly = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
|
||||||
|
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO && notAudioOnly)
|
||||||
|
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
|
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
|
||||||
|
@ -746,7 +765,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
.build()
|
.build()
|
||||||
imageLoader.enqueue(imageRequest)
|
imageLoader.enqueue(imageRequest)
|
||||||
}
|
}
|
||||||
if (isPlayingVideoLocally) {
|
if (isPlayingVideoLocally && (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
|
||||||
(activity as MainActivity).bottomSheet.setLocked(true)
|
(activity as MainActivity).bottomSheet.setLocked(true)
|
||||||
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||||
} else {
|
} else {
|
||||||
|
@ -765,6 +784,424 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the description of a Playable object in a Webview.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
class PlayerDetailsFragment : Fragment() {
|
||||||
|
private lateinit var shownoteView: ShownotesWebView
|
||||||
|
private var shownotesCleaner: ShownotesCleaner? = null
|
||||||
|
|
||||||
|
private var _binding: PlayerDetailsFragmentBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private var prevItem: Episode? = null
|
||||||
|
private var playable: Playable? = null
|
||||||
|
private var currentItem: Episode? = null
|
||||||
|
private var displayedChapterIndex = -1
|
||||||
|
|
||||||
|
private var cleanedNotes: String? = null
|
||||||
|
|
||||||
|
private var isLoading = false
|
||||||
|
private var homeText: String? = null
|
||||||
|
internal var showHomeText = false
|
||||||
|
internal var readerhtml: String? = null
|
||||||
|
|
||||||
|
private val currentChapter: Chapter?
|
||||||
|
get() {
|
||||||
|
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
||||||
|
return playable!!.getChapters()[displayedChapterIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
Logd(TAG, "fragment onCreateView")
|
||||||
|
_binding = PlayerDetailsFragmentBinding.inflate(inflater)
|
||||||
|
|
||||||
|
val colorFilter: ColorFilter? = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(binding.txtvPodcastTitle.currentTextColor, BlendModeCompat.SRC_IN)
|
||||||
|
binding.butNextChapter.colorFilter = colorFilter
|
||||||
|
binding.butPrevChapter.colorFilter = colorFilter
|
||||||
|
binding.chapterButton.setOnClickListener { ChaptersFragment().show(childFragmentManager, ChaptersFragment.TAG) }
|
||||||
|
binding.butPrevChapter.setOnClickListener { seekToPrevChapter() }
|
||||||
|
binding.butNextChapter.setOnClickListener { seekToNextChapter() }
|
||||||
|
|
||||||
|
Logd(TAG, "fragment onCreateView")
|
||||||
|
shownoteView = binding.webview
|
||||||
|
shownoteView.setTimecodeSelectedListener { time: Int -> seekTo(time) }
|
||||||
|
shownoteView.setPageFinishedListener {
|
||||||
|
// Restoring the scroll position might not always work
|
||||||
|
shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.addOnLayoutChangeListener(object : OnLayoutChangeListener {
|
||||||
|
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||||
|
if (binding.root.measuredHeight != shownoteView.minimumHeight) shownoteView.setMinimumHeight(binding.root.measuredHeight)
|
||||||
|
binding.root.removeOnLayoutChangeListener(this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
registerForContextMenu(shownoteView)
|
||||||
|
shownotesCleaner = ShownotesCleaner(requireContext())
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// override fun onStart() {
|
||||||
|
// Logd(TAG, "onStart()")
|
||||||
|
// super.onStart()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// override fun onStop() {
|
||||||
|
// Logd(TAG, "onStop()")
|
||||||
|
// super.onStop()
|
||||||
|
// }
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
Logd(TAG, "onDestroyView")
|
||||||
|
_binding = null
|
||||||
|
prevItem = null
|
||||||
|
currentItem = null
|
||||||
|
Logd(TAG, "Fragment destroyed")
|
||||||
|
shownoteView.removeAllViews()
|
||||||
|
shownoteView.destroy()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||||
|
return shownoteView.onContextItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun updateInfo() {
|
||||||
|
// if (isLoading) return
|
||||||
|
lifecycleScope.launch {
|
||||||
|
Logd(TAG, "in updateInfo")
|
||||||
|
isLoading = true
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (currentItem == null) {
|
||||||
|
playable = curMedia
|
||||||
|
if (playable != null && playable is EpisodeMedia) {
|
||||||
|
val episodeMedia = playable as EpisodeMedia
|
||||||
|
currentItem = episodeMedia.episodeOrFetch()
|
||||||
|
showHomeText = false
|
||||||
|
homeText = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentItem != null) {
|
||||||
|
playable = currentItem!!.media
|
||||||
|
if (prevItem?.identifier != currentItem!!.identifier) cleanedNotes = null
|
||||||
|
if (cleanedNotes == null) {
|
||||||
|
Logd(TAG, "calling load description ${currentItem!!.description==null} ${currentItem!!.title}")
|
||||||
|
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration()?:0)
|
||||||
|
}
|
||||||
|
prevItem = currentItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Logd(TAG, "subscribe: ${playable?.getEpisodeTitle()}")
|
||||||
|
displayMediaInfo(playable!!)
|
||||||
|
shownoteView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
||||||
|
Logd(TAG, "Webview loaded")
|
||||||
|
}
|
||||||
|
}.invokeOnCompletion { throwable ->
|
||||||
|
isLoading = false
|
||||||
|
if (throwable != null) Log.e(TAG, Log.getStackTraceString(throwable))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildHomeReaderText() {
|
||||||
|
showHomeText = !showHomeText
|
||||||
|
runOnIOScope {
|
||||||
|
if (showHomeText) {
|
||||||
|
homeText = currentItem!!.transcript
|
||||||
|
if (homeText == null && currentItem?.link != null) {
|
||||||
|
val url = currentItem!!.link!!
|
||||||
|
val htmlSource = fetchHtmlSource(url)
|
||||||
|
val readability4J = Readability4J(currentItem!!.link!!, htmlSource)
|
||||||
|
val article = readability4J.parse()
|
||||||
|
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||||
|
if (!readerhtml.isNullOrEmpty()) {
|
||||||
|
currentItem = upsertBlk(currentItem!!) {
|
||||||
|
it.setTranscriptIfLonger(readerhtml)
|
||||||
|
}
|
||||||
|
homeText = currentItem!!.transcript
|
||||||
|
// persistEpisode(currentItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!homeText.isNullOrEmpty()) {
|
||||||
|
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
||||||
|
cleanedNotes = shownotesCleaner?.processShownotes(homeText!!, 0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
||||||
|
cleanedNotes ?: "No notes",
|
||||||
|
"text/html",
|
||||||
|
"UTF-8",
|
||||||
|
null)
|
||||||
|
}
|
||||||
|
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
||||||
|
} else {
|
||||||
|
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
||||||
|
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration() ?: 0)
|
||||||
|
if (!cleanedNotes.isNullOrEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
||||||
|
cleanedNotes ?: "No notes",
|
||||||
|
"text/html",
|
||||||
|
"UTF-8",
|
||||||
|
null)
|
||||||
|
}
|
||||||
|
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi private fun displayMediaInfo(media: Playable) {
|
||||||
|
Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}")
|
||||||
|
val pubDateStr = MiscFormatter.formatAbbrev(context, media.getPubDate())
|
||||||
|
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
|
||||||
|
if (media is EpisodeMedia) {
|
||||||
|
if (currentItem?.feedId != null) {
|
||||||
|
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), currentItem!!.feedId!!)
|
||||||
|
binding.txtvPodcastTitle.setOnClickListener { startActivity(openFeed) }
|
||||||
|
}
|
||||||
|
} else binding.txtvPodcastTitle.setOnClickListener(null)
|
||||||
|
|
||||||
|
binding.txtvPodcastTitle.setOnLongClickListener { copyText(media.getFeedTitle()) }
|
||||||
|
binding.episodeDate.text = StringUtils.stripToEmpty(pubDateStr)
|
||||||
|
binding.txtvEpisodeTitle.text = currentItem?.title
|
||||||
|
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(currentItem?.title?:"") }
|
||||||
|
binding.txtvEpisodeTitle.setOnClickListener {
|
||||||
|
val lines = binding.txtvEpisodeTitle.lineCount
|
||||||
|
val animUnit = 1500
|
||||||
|
if (lines > binding.txtvEpisodeTitle.maxLines) {
|
||||||
|
val titleHeight = (binding.txtvEpisodeTitle.height - binding.txtvEpisodeTitle.paddingTop - binding.txtvEpisodeTitle.paddingBottom)
|
||||||
|
val verticalMarquee: ObjectAnimator = ObjectAnimator.ofInt(binding.txtvEpisodeTitle, "scrollY", 0,
|
||||||
|
(lines - binding.txtvEpisodeTitle.maxLines) * (titleHeight / binding.txtvEpisodeTitle.maxLines)).setDuration((lines * animUnit).toLong())
|
||||||
|
val fadeOut: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 0f)
|
||||||
|
fadeOut.setStartDelay(animUnit.toLong())
|
||||||
|
fadeOut.addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
binding.txtvEpisodeTitle.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
val fadeBackIn: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 1f)
|
||||||
|
val set = AnimatorSet()
|
||||||
|
set.playSequentially(verticalMarquee, fadeOut, fadeBackIn)
|
||||||
|
set.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displayedChapterIndex = -1
|
||||||
|
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())) //calls displayCoverImage
|
||||||
|
updateChapterControlVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateChapterControlVisibility() {
|
||||||
|
var chapterControlVisible = false
|
||||||
|
when {
|
||||||
|
playable?.getChapters() != null -> chapterControlVisible = playable!!.getChapters().isNotEmpty()
|
||||||
|
playable is EpisodeMedia -> {
|
||||||
|
val item_ = (playable as EpisodeMedia).episodeOrFetch()
|
||||||
|
// If an item has chapters but they are not loaded yet, still display the button.
|
||||||
|
chapterControlVisible = !item_?.chapters.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val newVisibility = if (chapterControlVisible) View.VISIBLE else View.GONE
|
||||||
|
if (binding.chapterButton.visibility != newVisibility) {
|
||||||
|
binding.chapterButton.visibility = newVisibility
|
||||||
|
ObjectAnimator.ofFloat(binding.chapterButton, "alpha",
|
||||||
|
(if (chapterControlVisible) 0 else 1).toFloat(), (if (chapterControlVisible) 1 else 0).toFloat()).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshChapterData(chapterIndex: Int) {
|
||||||
|
Logd(TAG, "in refreshChapterData $chapterIndex")
|
||||||
|
if (playable != null && chapterIndex > -1) {
|
||||||
|
if (playable!!.getPosition() > playable!!.getDuration() || chapterIndex >= playable!!.getChapters().size - 1) {
|
||||||
|
displayedChapterIndex = playable!!.getChapters().size - 1
|
||||||
|
binding.butNextChapter.visibility = View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
displayedChapterIndex = chapterIndex
|
||||||
|
binding.butNextChapter.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displayCoverImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayCoverImage() {
|
||||||
|
if (playable == null) return
|
||||||
|
if (displayedChapterIndex == -1 || playable!!.getChapters().isEmpty() || playable!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
|
||||||
|
val imageLoader = binding.imgvCover.context.imageLoader
|
||||||
|
val imageRequest = ImageRequest.Builder(requireContext())
|
||||||
|
.data(playable!!.getImageLocation())
|
||||||
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
|
.placeholder(R.color.light_gray)
|
||||||
|
.listener(object : ImageRequest.Listener {
|
||||||
|
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||||
|
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||||
|
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||||
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
|
.error(R.mipmap.ic_launcher)
|
||||||
|
.target(binding.imgvCover)
|
||||||
|
.build()
|
||||||
|
imageLoader.enqueue(fallbackImageRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.target(binding.imgvCover)
|
||||||
|
.build()
|
||||||
|
imageLoader.enqueue(imageRequest)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val imgLoc = EmbeddedChapterImage.getModelFor(playable!!, displayedChapterIndex)
|
||||||
|
val imageLoader = binding.imgvCover.context.imageLoader
|
||||||
|
val imageRequest = ImageRequest.Builder(requireContext())
|
||||||
|
.data(imgLoc)
|
||||||
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
|
.placeholder(R.color.light_gray)
|
||||||
|
.listener(object : ImageRequest.Listener {
|
||||||
|
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||||
|
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||||
|
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||||
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
|
.error(R.mipmap.ic_launcher)
|
||||||
|
.target(binding.imgvCover)
|
||||||
|
.build()
|
||||||
|
imageLoader.enqueue(fallbackImageRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.target(binding.imgvCover)
|
||||||
|
.build()
|
||||||
|
imageLoader.enqueue(imageRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi private fun seekToPrevChapter() {
|
||||||
|
val curr: Chapter? = currentChapter
|
||||||
|
if (curr == null || displayedChapterIndex == -1) return
|
||||||
|
|
||||||
|
when {
|
||||||
|
displayedChapterIndex < 1 -> seekTo(0)
|
||||||
|
(curPositionFB - 10000 * curSpeedFB) < curr.start -> {
|
||||||
|
refreshChapterData(displayedChapterIndex - 1)
|
||||||
|
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||||
|
}
|
||||||
|
else -> seekTo(curr.start.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi private fun seekToNextChapter() {
|
||||||
|
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= playable!!.getChapters().size) return
|
||||||
|
refreshChapterData(displayedChapterIndex + 1)
|
||||||
|
seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@UnstableApi override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
savePreference()
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi private fun savePreference() {
|
||||||
|
Logd(TAG, "Saving preferences")
|
||||||
|
val editor = prefs?.edit() ?: return
|
||||||
|
if (curMedia != null) {
|
||||||
|
Logd(TAG, "Saving scroll position: " + binding.itemDescriptionFragment.scrollY)
|
||||||
|
editor.putInt(PREF_SCROLL_Y, binding.itemDescriptionFragment.scrollY)
|
||||||
|
editor.putString(PREF_PLAYABLE_ID, curMedia!!.getIdentifier().toString())
|
||||||
|
} else {
|
||||||
|
Logd(TAG, "savePreferences was called while media or webview was null")
|
||||||
|
editor.putInt(PREF_SCROLL_Y, -1)
|
||||||
|
editor.putString(PREF_PLAYABLE_ID, "")
|
||||||
|
}
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi private fun restoreFromPreference(): Boolean {
|
||||||
|
if ((activity as MainActivity).bottomSheet.state != BottomSheetBehavior.STATE_EXPANDED) return false
|
||||||
|
|
||||||
|
Logd(TAG, "Restoring from preferences")
|
||||||
|
val activity: Activity? = activity
|
||||||
|
if (activity != null) {
|
||||||
|
val id = prefs!!.getString(PREF_PLAYABLE_ID, "")
|
||||||
|
val scrollY = prefs!!.getInt(PREF_SCROLL_Y, -1)
|
||||||
|
if (scrollY != -1) {
|
||||||
|
if (id == curMedia?.getIdentifier()?.toString()) {
|
||||||
|
Logd(TAG, "Restored scroll Position: $scrollY")
|
||||||
|
binding.itemDescriptionFragment.scrollTo(binding.itemDescriptionFragment.scrollX, scrollY)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
Logd(TAG, "reset scroll Position: 0")
|
||||||
|
binding.itemDescriptionFragment.scrollTo(0, 0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scrollToTop() {
|
||||||
|
binding.itemDescriptionFragment.scrollTo(0, 0)
|
||||||
|
savePreference()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
|
if (playable?.getIdentifier() != event.media?.getIdentifier()) return
|
||||||
|
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(playable, event.position)
|
||||||
|
if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) refreshChapterData(newChapterIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItem(item_: Episode) {
|
||||||
|
Logd(TAG, "setItem ${item_.title}")
|
||||||
|
if (currentItem?.identifier != item_.identifier) {
|
||||||
|
currentItem = item_
|
||||||
|
showHomeText = false
|
||||||
|
homeText = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
// super.onConfigurationChanged(newConfig)
|
||||||
|
// configureForOrientation(newConfig)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private fun configureForOrientation(newConfig: Configuration) {
|
||||||
|
// val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
|
//
|
||||||
|
//// binding.coverFragment.orientation = if (isPortrait) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
|
||||||
|
//
|
||||||
|
// if (isPortrait) {
|
||||||
|
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||||
|
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
// } else {
|
||||||
|
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||||
|
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// (binding.episodeDetails.parent as ViewGroup).removeView(binding.episodeDetails)
|
||||||
|
// if (isPortrait) {
|
||||||
|
// binding.coverFragment.addView(binding.episodeDetails)
|
||||||
|
// } else {
|
||||||
|
// binding.coverFragmentTextContainer.addView(binding.episodeDetails)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@UnstableApi private fun copyText(text: String): Boolean {
|
||||||
|
val clipboardManager: ClipboardManager? = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java)
|
||||||
|
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Podcini", text))
|
||||||
|
if (Build.VERSION.SDK_INT <= 32) {
|
||||||
|
(requireActivity() as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = PlayerDetailsFragment::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
|
private const val PREF = "ItemDescriptionFragmentPrefs"
|
||||||
|
private const val PREF_SCROLL_Y = "prefScrollY"
|
||||||
|
private const val PREF_PLAYABLE_ID = "prefPlayableId"
|
||||||
|
|
||||||
|
var prefs: SharedPreferences? = null
|
||||||
|
fun getSharedPrefs(context: Context) {
|
||||||
|
if (prefs == null) prefs = context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = AudioPlayerFragment::class.simpleName ?: "Anonymous"
|
val TAG = AudioPlayerFragment::class.simpleName ?: "Anonymous"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,470 +0,0 @@
|
||||||
package ac.mdiq.podcini.ui.fragment
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
|
||||||
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
|
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
|
||||||
import ac.mdiq.podcini.storage.model.*
|
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
|
||||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
|
||||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
|
||||||
import ac.mdiq.podcini.util.MiscFormatter
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.animation.AnimatorSet
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.*
|
|
||||||
import android.graphics.ColorFilter
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.OnLayoutChangeListener
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
|
||||||
import androidx.core.graphics.BlendModeCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import coil.imageLoader
|
|
||||||
import coil.request.ErrorResult
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.dankito.readability4j.Readability4J
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the description of a Playable object in a Webview.
|
|
||||||
*/
|
|
||||||
@UnstableApi
|
|
||||||
class PlayerDetailsFragment : Fragment() {
|
|
||||||
private lateinit var shownoteView: ShownotesWebView
|
|
||||||
private var shownotesCleaner: ShownotesCleaner? = null
|
|
||||||
|
|
||||||
private var _binding: PlayerDetailsFragmentBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private var prevItem: Episode? = null
|
|
||||||
private var playable: Playable? = null
|
|
||||||
private var currentItem: Episode? = null
|
|
||||||
private var displayedChapterIndex = -1
|
|
||||||
|
|
||||||
private var cleanedNotes: String? = null
|
|
||||||
|
|
||||||
private var isLoading = false
|
|
||||||
private var homeText: String? = null
|
|
||||||
internal var showHomeText = false
|
|
||||||
internal var readerhtml: String? = null
|
|
||||||
|
|
||||||
private val currentChapter: Chapter?
|
|
||||||
get() {
|
|
||||||
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
|
||||||
return playable!!.getChapters()[displayedChapterIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
Logd(TAG, "fragment onCreateView")
|
|
||||||
_binding = PlayerDetailsFragmentBinding.inflate(inflater)
|
|
||||||
|
|
||||||
val colorFilter: ColorFilter? = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(binding.txtvPodcastTitle.currentTextColor, BlendModeCompat.SRC_IN)
|
|
||||||
binding.butNextChapter.colorFilter = colorFilter
|
|
||||||
binding.butPrevChapter.colorFilter = colorFilter
|
|
||||||
binding.chapterButton.setOnClickListener { ChaptersFragment().show(childFragmentManager, ChaptersFragment.TAG) }
|
|
||||||
binding.butPrevChapter.setOnClickListener { seekToPrevChapter() }
|
|
||||||
binding.butNextChapter.setOnClickListener { seekToNextChapter() }
|
|
||||||
|
|
||||||
Logd(TAG, "fragment onCreateView")
|
|
||||||
shownoteView = binding.webview
|
|
||||||
shownoteView.setTimecodeSelectedListener { time: Int -> seekTo(time) }
|
|
||||||
shownoteView.setPageFinishedListener {
|
|
||||||
// Restoring the scroll position might not always work
|
|
||||||
shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.addOnLayoutChangeListener(object : OnLayoutChangeListener {
|
|
||||||
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
|
||||||
if (binding.root.measuredHeight != shownoteView.minimumHeight) shownoteView.setMinimumHeight(binding.root.measuredHeight)
|
|
||||||
binding.root.removeOnLayoutChangeListener(this)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
registerForContextMenu(shownoteView)
|
|
||||||
shownotesCleaner = ShownotesCleaner(requireContext())
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// override fun onStart() {
|
|
||||||
// Logd(TAG, "onStart()")
|
|
||||||
// super.onStart()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// override fun onStop() {
|
|
||||||
// Logd(TAG, "onStop()")
|
|
||||||
// super.onStop()
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
Logd(TAG, "onDestroyView")
|
|
||||||
_binding = null
|
|
||||||
prevItem = null
|
|
||||||
currentItem = null
|
|
||||||
Logd(TAG, "Fragment destroyed")
|
|
||||||
shownoteView.removeAllViews()
|
|
||||||
shownoteView.destroy()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
|
||||||
return shownoteView.onContextItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun updateInfo() {
|
|
||||||
// if (isLoading) return
|
|
||||||
lifecycleScope.launch {
|
|
||||||
Logd(TAG, "in updateInfo")
|
|
||||||
isLoading = true
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
if (currentItem == null) {
|
|
||||||
playable = curMedia
|
|
||||||
if (playable != null && playable is EpisodeMedia) {
|
|
||||||
val episodeMedia = playable as EpisodeMedia
|
|
||||||
currentItem = episodeMedia.episodeOrFetch()
|
|
||||||
showHomeText = false
|
|
||||||
homeText = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentItem != null) {
|
|
||||||
playable = currentItem!!.media
|
|
||||||
if (prevItem?.identifier != currentItem!!.identifier) cleanedNotes = null
|
|
||||||
if (cleanedNotes == null) {
|
|
||||||
Logd(TAG, "calling load description ${currentItem!!.description==null} ${currentItem!!.title}")
|
|
||||||
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration()?:0)
|
|
||||||
}
|
|
||||||
prevItem = currentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Logd(TAG, "subscribe: ${playable?.getEpisodeTitle()}")
|
|
||||||
displayMediaInfo(playable!!)
|
|
||||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
|
||||||
Logd(TAG, "Webview loaded")
|
|
||||||
}
|
|
||||||
}.invokeOnCompletion { throwable ->
|
|
||||||
isLoading = false
|
|
||||||
if (throwable != null) Log.e(TAG, Log.getStackTraceString(throwable))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildHomeReaderText() {
|
|
||||||
showHomeText = !showHomeText
|
|
||||||
runOnIOScope {
|
|
||||||
if (showHomeText) {
|
|
||||||
homeText = currentItem!!.transcript
|
|
||||||
if (homeText == null && currentItem?.link != null) {
|
|
||||||
val url = currentItem!!.link!!
|
|
||||||
val htmlSource = fetchHtmlSource(url)
|
|
||||||
val readability4J = Readability4J(currentItem!!.link!!, htmlSource)
|
|
||||||
val article = readability4J.parse()
|
|
||||||
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
|
||||||
if (!readerhtml.isNullOrEmpty()) {
|
|
||||||
currentItem = upsertBlk(currentItem!!) {
|
|
||||||
it.setTranscriptIfLonger(readerhtml)
|
|
||||||
}
|
|
||||||
homeText = currentItem!!.transcript
|
|
||||||
// persistEpisode(currentItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!homeText.isNullOrEmpty()) {
|
|
||||||
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
|
||||||
cleanedNotes = shownotesCleaner?.processShownotes(homeText!!, 0)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
|
||||||
cleanedNotes ?: "No notes",
|
|
||||||
"text/html",
|
|
||||||
"UTF-8",
|
|
||||||
null)
|
|
||||||
}
|
|
||||||
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
|
||||||
} else {
|
|
||||||
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
|
||||||
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration() ?: 0)
|
|
||||||
if (!cleanedNotes.isNullOrEmpty()) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
|
||||||
cleanedNotes ?: "No notes",
|
|
||||||
"text/html",
|
|
||||||
"UTF-8",
|
|
||||||
null)
|
|
||||||
}
|
|
||||||
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi private fun displayMediaInfo(media: Playable) {
|
|
||||||
Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}")
|
|
||||||
val pubDateStr = MiscFormatter.formatAbbrev(context, media.getPubDate())
|
|
||||||
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
|
|
||||||
if (media is EpisodeMedia) {
|
|
||||||
if (currentItem?.feedId != null) {
|
|
||||||
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), currentItem!!.feedId!!)
|
|
||||||
binding.txtvPodcastTitle.setOnClickListener { startActivity(openFeed) }
|
|
||||||
}
|
|
||||||
} else binding.txtvPodcastTitle.setOnClickListener(null)
|
|
||||||
|
|
||||||
binding.txtvPodcastTitle.setOnLongClickListener { copyText(media.getFeedTitle()) }
|
|
||||||
binding.episodeDate.text = StringUtils.stripToEmpty(pubDateStr)
|
|
||||||
binding.txtvEpisodeTitle.text = currentItem?.title
|
|
||||||
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(currentItem?.title?:"") }
|
|
||||||
binding.txtvEpisodeTitle.setOnClickListener {
|
|
||||||
val lines = binding.txtvEpisodeTitle.lineCount
|
|
||||||
val animUnit = 1500
|
|
||||||
if (lines > binding.txtvEpisodeTitle.maxLines) {
|
|
||||||
val titleHeight = (binding.txtvEpisodeTitle.height - binding.txtvEpisodeTitle.paddingTop - binding.txtvEpisodeTitle.paddingBottom)
|
|
||||||
val verticalMarquee: ObjectAnimator = ObjectAnimator.ofInt(binding.txtvEpisodeTitle, "scrollY", 0,
|
|
||||||
(lines - binding.txtvEpisodeTitle.maxLines) * (titleHeight / binding.txtvEpisodeTitle.maxLines)).setDuration((lines * animUnit).toLong())
|
|
||||||
val fadeOut: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 0f)
|
|
||||||
fadeOut.setStartDelay(animUnit.toLong())
|
|
||||||
fadeOut.addListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
binding.txtvEpisodeTitle.scrollTo(0, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
val fadeBackIn: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 1f)
|
|
||||||
val set = AnimatorSet()
|
|
||||||
set.playSequentially(verticalMarquee, fadeOut, fadeBackIn)
|
|
||||||
set.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
displayedChapterIndex = -1
|
|
||||||
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())) //calls displayCoverImage
|
|
||||||
updateChapterControlVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateChapterControlVisibility() {
|
|
||||||
var chapterControlVisible = false
|
|
||||||
when {
|
|
||||||
playable?.getChapters() != null -> chapterControlVisible = playable!!.getChapters().isNotEmpty()
|
|
||||||
playable is EpisodeMedia -> {
|
|
||||||
val item_ = (playable as EpisodeMedia).episodeOrFetch()
|
|
||||||
// If an item has chapters but they are not loaded yet, still display the button.
|
|
||||||
chapterControlVisible = !item_?.chapters.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val newVisibility = if (chapterControlVisible) View.VISIBLE else View.GONE
|
|
||||||
if (binding.chapterButton.visibility != newVisibility) {
|
|
||||||
binding.chapterButton.visibility = newVisibility
|
|
||||||
ObjectAnimator.ofFloat(binding.chapterButton, "alpha",
|
|
||||||
(if (chapterControlVisible) 0 else 1).toFloat(), (if (chapterControlVisible) 1 else 0).toFloat()).start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshChapterData(chapterIndex: Int) {
|
|
||||||
Logd(TAG, "in refreshChapterData $chapterIndex")
|
|
||||||
if (playable != null && chapterIndex > -1) {
|
|
||||||
if (playable!!.getPosition() > playable!!.getDuration() || chapterIndex >= playable!!.getChapters().size - 1) {
|
|
||||||
displayedChapterIndex = playable!!.getChapters().size - 1
|
|
||||||
binding.butNextChapter.visibility = View.INVISIBLE
|
|
||||||
} else {
|
|
||||||
displayedChapterIndex = chapterIndex
|
|
||||||
binding.butNextChapter.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
displayCoverImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun displayCoverImage() {
|
|
||||||
if (playable == null) return
|
|
||||||
if (displayedChapterIndex == -1 || playable!!.getChapters().isEmpty() || playable!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
|
|
||||||
val imageLoader = binding.imgvCover.context.imageLoader
|
|
||||||
val imageRequest = ImageRequest.Builder(requireContext())
|
|
||||||
.data(playable!!.getImageLocation())
|
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
|
||||||
.placeholder(R.color.light_gray)
|
|
||||||
.listener(object : ImageRequest.Listener {
|
|
||||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
|
||||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
|
||||||
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
|
||||||
.error(R.mipmap.ic_launcher)
|
|
||||||
.target(binding.imgvCover)
|
|
||||||
.build()
|
|
||||||
imageLoader.enqueue(fallbackImageRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.target(binding.imgvCover)
|
|
||||||
.build()
|
|
||||||
imageLoader.enqueue(imageRequest)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
val imgLoc = EmbeddedChapterImage.getModelFor(playable!!, displayedChapterIndex)
|
|
||||||
val imageLoader = binding.imgvCover.context.imageLoader
|
|
||||||
val imageRequest = ImageRequest.Builder(requireContext())
|
|
||||||
.data(imgLoc)
|
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
|
||||||
.placeholder(R.color.light_gray)
|
|
||||||
.listener(object : ImageRequest.Listener {
|
|
||||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
|
||||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
|
||||||
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
|
||||||
.error(R.mipmap.ic_launcher)
|
|
||||||
.target(binding.imgvCover)
|
|
||||||
.build()
|
|
||||||
imageLoader.enqueue(fallbackImageRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.target(binding.imgvCover)
|
|
||||||
.build()
|
|
||||||
imageLoader.enqueue(imageRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi private fun seekToPrevChapter() {
|
|
||||||
val curr: Chapter? = currentChapter
|
|
||||||
if (curr == null || displayedChapterIndex == -1) return
|
|
||||||
|
|
||||||
when {
|
|
||||||
displayedChapterIndex < 1 -> seekTo(0)
|
|
||||||
(curPositionFB - 10000 * curSpeedFB) < curr.start -> {
|
|
||||||
refreshChapterData(displayedChapterIndex - 1)
|
|
||||||
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
|
||||||
}
|
|
||||||
else -> seekTo(curr.start.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi private fun seekToNextChapter() {
|
|
||||||
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= playable!!.getChapters().size) return
|
|
||||||
refreshChapterData(displayedChapterIndex + 1)
|
|
||||||
seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@UnstableApi override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
savePreference()
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi private fun savePreference() {
|
|
||||||
Logd(TAG, "Saving preferences")
|
|
||||||
val editor = prefs?.edit() ?: return
|
|
||||||
if (curMedia != null) {
|
|
||||||
Logd(TAG, "Saving scroll position: " + binding.itemDescriptionFragment.scrollY)
|
|
||||||
editor.putInt(PREF_SCROLL_Y, binding.itemDescriptionFragment.scrollY)
|
|
||||||
editor.putString(PREF_PLAYABLE_ID, curMedia!!.getIdentifier().toString())
|
|
||||||
} else {
|
|
||||||
Logd(TAG, "savePreferences was called while media or webview was null")
|
|
||||||
editor.putInt(PREF_SCROLL_Y, -1)
|
|
||||||
editor.putString(PREF_PLAYABLE_ID, "")
|
|
||||||
}
|
|
||||||
editor.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi private fun restoreFromPreference(): Boolean {
|
|
||||||
if ((activity as MainActivity).bottomSheet.state != BottomSheetBehavior.STATE_EXPANDED) return false
|
|
||||||
|
|
||||||
Logd(TAG, "Restoring from preferences")
|
|
||||||
val activity: Activity? = activity
|
|
||||||
if (activity != null) {
|
|
||||||
val id = prefs!!.getString(PREF_PLAYABLE_ID, "")
|
|
||||||
val scrollY = prefs!!.getInt(PREF_SCROLL_Y, -1)
|
|
||||||
if (scrollY != -1) {
|
|
||||||
if (id == curMedia?.getIdentifier()?.toString()) {
|
|
||||||
Logd(TAG, "Restored scroll Position: $scrollY")
|
|
||||||
binding.itemDescriptionFragment.scrollTo(binding.itemDescriptionFragment.scrollX, scrollY)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
Logd(TAG, "reset scroll Position: 0")
|
|
||||||
binding.itemDescriptionFragment.scrollTo(0, 0)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scrollToTop() {
|
|
||||||
binding.itemDescriptionFragment.scrollTo(0, 0)
|
|
||||||
savePreference()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
|
||||||
if (playable?.getIdentifier() != event.media?.getIdentifier()) return
|
|
||||||
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(playable, event.position)
|
|
||||||
if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) refreshChapterData(newChapterIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItem(item_: Episode) {
|
|
||||||
Logd(TAG, "setItem ${item_.title}")
|
|
||||||
if (currentItem?.identifier != item_.identifier) {
|
|
||||||
currentItem = item_
|
|
||||||
showHomeText = false
|
|
||||||
homeText = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
// super.onConfigurationChanged(newConfig)
|
|
||||||
// configureForOrientation(newConfig)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun configureForOrientation(newConfig: Configuration) {
|
|
||||||
// val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
|
|
||||||
//
|
|
||||||
//// binding.coverFragment.orientation = if (isPortrait) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
|
|
||||||
//
|
|
||||||
// if (isPortrait) {
|
|
||||||
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
|
|
||||||
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
||||||
// } else {
|
|
||||||
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
|
||||||
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// (binding.episodeDetails.parent as ViewGroup).removeView(binding.episodeDetails)
|
|
||||||
// if (isPortrait) {
|
|
||||||
// binding.coverFragment.addView(binding.episodeDetails)
|
|
||||||
// } else {
|
|
||||||
// binding.coverFragmentTextContainer.addView(binding.episodeDetails)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
@UnstableApi private fun copyText(text: String): Boolean {
|
|
||||||
val clipboardManager: ClipboardManager? = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java)
|
|
||||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Podcini", text))
|
|
||||||
if (Build.VERSION.SDK_INT <= 32) {
|
|
||||||
(requireActivity() as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = PlayerDetailsFragment::class.simpleName ?: "Anonymous"
|
|
||||||
|
|
||||||
private const val PREF = "ItemDescriptionFragmentPrefs"
|
|
||||||
private const val PREF_SCROLL_Y = "prefScrollY"
|
|
||||||
private const val PREF_PLAYABLE_ID = "prefPlayableId"
|
|
||||||
|
|
||||||
var prefs: SharedPreferences? = null
|
|
||||||
fun getSharedPrefs(context: Context) {
|
|
||||||
if (prefs == null) prefs = context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
# 6.5.9
|
||||||
|
|
||||||
|
* partially fixed an issue seen on Samsung Android 14 device where after playing the user-started episode, subsequent episode in the queue is not played in foreground service and there is not notification panel and can get stopped by the system.
|
||||||
|
* the current fix is though the subsequent episodes are still played without notification, the play is not stopped by the system.
|
||||||
|
* if videoMode of a feed is set to "audio only",
|
||||||
|
* press on icon in the player UI will expand the player detailed view (rather than video view)
|
||||||
|
* "show video" on the menu of AudioPlayer view is hidden
|
||||||
|
* some class restructuring
|
||||||
|
|
||||||
# 6.5.8
|
# 6.5.8
|
||||||
|
|
||||||
* corrected mis-behavior of speed settings for video media
|
* corrected mis-behavior of speed settings for video media
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
Version 6.5.9 brings several changes:
|
||||||
|
|
||||||
|
* partially fixed an issue seen on Samsung Android 14 device where after playing the user-started episode, subsequent episode in the queue is not played in foreground service and there is not notification panel and can get stopped by the system.
|
||||||
|
* the current fix is though the subsequent episodes are still played without notification, the play is not stopped by the system.
|
||||||
|
* if videoMode of a feed is set to "audio only",
|
||||||
|
* press on icon in the player UI will expand the player detailed view (rather than video view)
|
||||||
|
* "show video" on the menu of AudioPlayer view is hidden
|
||||||
|
* some class restructuring
|
|
@ -7,5 +7,5 @@ android.nonFinalResIds=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
|
|
||||||
org.gradle.jvmargs=-Xmx2048m
|
org.gradle.jvmargs=-Xmx2048m
|
||||||
|
kotlin.daemon.jvmargs=-Xmx1g
|
||||||
org.gradle.configuration-cache=true
|
org.gradle.configuration-cache=true
|
||||||
|
|
Loading…
Reference in New Issue