6.5.9 commit

This commit is contained in:
Xilin Jia 2024-09-10 11:29:19 +01:00
parent e3f7c31407
commit fb114f349b
9 changed files with 504 additions and 522 deletions

View File

@ -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.
#### 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:

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020242
versionName "6.5.8"
versionCode 3020243
versionName "6.5.9"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -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.EpisodeUtil
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.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.utils.NotificationUtils
@ -738,13 +739,10 @@ class PlaybackService : MediaLibraryService() {
override fun onTaskRemoved(rootIntent: Intent?) {
Logd(TAG, "onTaskRemoved")
val player = mediaSession?.player
if (player != null) {
if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == STATE_ENDED) {
// Stop the service if not playing, continue playing in the background
// otherwise.
stopSelf()
}
val player = mediaSession?.player ?: return
if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == STATE_ENDED) {
// Stop the service if not playing, continue playing in the background otherwise.
stopSelf()
}
}
@ -2569,7 +2567,9 @@ class PlaybackService : MediaLibraryService() {
playbackService?.mPlayer?.prepare()
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")
}
}
}

View File

@ -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.sync.queue.SynchronizationQueueSink
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.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer
@ -36,6 +37,7 @@ import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.Manifest
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@ -64,6 +66,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
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.dialog.MaterialAlertDialogBuilder
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.flow.collectLatest
import org.apache.commons.lang3.ArrayUtils
@ -93,6 +99,7 @@ class MainActivity : CastEnabledActivity() {
private lateinit var audioPlayerView: View
private lateinit var navDrawer: View
private lateinit var dummyView : View
private lateinit var controllerFuture: ListenableFuture<MediaController>
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
private set
@ -174,7 +181,7 @@ class MainActivity : CastEnabledActivity() {
buildTags()
monitorFeeds()
// InTheatre.apply { }
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
AudioPlayerFragment.PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
PlayerWidget.getSharedPrefs(this@MainActivity)
StatisticsFragment.getSharedPrefs(this@MainActivity)
OnlineFeedFragment.getSharedPrefs(this@MainActivity)
@ -189,13 +196,10 @@ class MainActivity : CastEnabledActivity() {
// setContentView(R.layout.main_activity)
setContentView(binding.root)
recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25)
dummyView = object : View(this) {}
drawerLayout = findViewById(R.id.main_layout)
navDrawer = findViewById(R.id.navDrawerFragment)
setNavDrawerSize()
mainView = findViewById(R.id.main_view)
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 ->
navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
updateInsets()
WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE)
.build()
WindowInsetsCompat.Builder(insets).setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE).build()
}
val fm = supportFragmentManager
@ -220,14 +222,9 @@ class MainActivity : CastEnabledActivity() {
val lastFragment = NavDrawerFragment.getLastNavFragment()
if (ArrayUtils.contains(NavDrawerFragment.NAV_DRAWER_TAGS, lastFragment)) loadFragment(lastFragment, null)
else {
try {
loadFeedFragmentById(lastFragment.toInt().toLong(), null)
} catch (e: NumberFormatException) {
// 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)
}
// it's not a number, this happens if we removed a label from the NAV_DRAWER_TAGS give them a nice default...
try { loadFeedFragmentById(lastFragment.toInt().toLong(), null) }
catch (e: NumberFormatException) { loadFragment(SubscriptionsFragment.TAG, null) }
}
}
}
@ -367,6 +364,7 @@ class MainActivity : CastEnabledActivity() {
_binding = null
// realm.close()
drawerLayout?.removeDrawerListener(drawerToggle!!)
MediaController.releaseFuture(controllerFuture)
super.onDestroy()
}
@ -467,14 +465,9 @@ class MainActivity : CastEnabledActivity() {
@JvmOverloads
fun loadChildFragment(fragment: Fragment, transition: TransitionEffect? = TransitionEffect.NONE) {
val transaction = supportFragmentManager.beginTransaction()
when (transition) {
TransitionEffect.FADE -> transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
TransitionEffect.SLIDE -> transaction.setCustomAnimations(
R.anim.slide_right_in,
R.anim.slide_left_out,
R.anim.slide_left_in,
R.anim.slide_right_out)
TransitionEffect.SLIDE -> transaction.setCustomAnimations(R.anim.slide_right_in, R.anim.slide_left_out, R.anim.slide_left_in, R.anim.slide_right_out)
TransitionEffect.NONE -> {}
null -> {}
}
@ -518,6 +511,12 @@ class MainActivity : CastEnabledActivity() {
super.onStart()
procFlowEvents()
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() {
@ -628,7 +627,6 @@ class MainActivity : CastEnabledActivity() {
val tag = intent.getStringExtra(MainActivityStarter.Extras.fragment_tag.name)
val args = intent.getBundleExtra(MainActivityStarter.Extras.fragment_args.name)
if (tag != null) loadFragment(tag, args)
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
}
intent.getBooleanExtra(MainActivityStarter.Extras.open_player.name, false) -> {

View File

@ -2,7 +2,9 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
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.ServiceStatusHandler
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.VideoMode
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.curPositionFB
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.toggleFallbackSpeed
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.playPause
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.toggleFallbackSpeed
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
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.utils.ChapterUtils
import ac.mdiq.podcini.storage.utils.DurationConverter
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.activity.MainActivity
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.SleepTimerDialog
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.PlayButton
import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow
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.content.ComponentName
import android.content.Intent
import android.content.*
import android.graphics.ColorFilter
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.View.OnLayoutChangeListener
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.widget.Toolbar
import androidx.cardview.widget.CardView
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.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import coil.imageLoader
import coil.request.ErrorResult
import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
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.MoreExecutors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.dankito.readability4j.Readability4J
import org.apache.commons.lang3.StringUtils
import java.text.DecimalFormat
import java.text.NumberFormat
import kotlin.math.max
@ -171,6 +188,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
_binding = null
controller?.release()
controller = null
// MediaController.releaseFuture(controllerFuture)
super.onDestroyView()
}
@ -293,12 +311,12 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
super.onStart()
procFlowEvents()
val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
controllerFuture.addListener({
// mediaController = controllerFuture.get()
// Logd(TAG, "controllerFuture.addListener: $mediaController")
}, MoreExecutors.directExecutor())
// val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
// controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
// controllerFuture.addListener({
//// mediaController = controllerFuture.get()
//// Logd(TAG, "controllerFuture.addListener: $mediaController")
// }, MoreExecutors.directExecutor())
loadMediaInfo(false)
}
@ -306,7 +324,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onStop() {
Logd(TAG, "onStop()")
super.onStop()
MediaController.releaseFuture(controllerFuture)
// MediaController.releaseFuture(controllerFuture)
cancelFlowEvents()
// 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)
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) {
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
@ -746,7 +765,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
.build()
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.setState(BottomSheetBehavior.STATE_COLLAPSED)
} 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 {
val TAG = AudioPlayerFragment::class.simpleName ?: "Anonymous"
}

View File

@ -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)
}
}
}

View File

@ -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
* corrected mis-behavior of speed settings for video media

View File

@ -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

View File

@ -7,5 +7,5 @@ android.nonFinalResIds=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
kotlin.daemon.jvmargs=-Xmx1g
org.gradle.configuration-cache=true