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

View File

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

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.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")
}
} }
} }

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.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) -> {

View File

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

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 # 6.5.8
* corrected mis-behavior of speed settings for video media * 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.caching=true
org.gradle.jvmargs=-Xmx2048m org.gradle.jvmargs=-Xmx2048m
kotlin.daemon.jvmargs=-Xmx1g
org.gradle.configuration-cache=true org.gradle.configuration-cache=true