6.5.9 commit
This commit is contained in:
parent
e3f7c31407
commit
fb114f349b
|
@ -16,7 +16,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
|||
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
||||
#### 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:
|
||||
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) -> {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -1,470 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
import ac.mdiq.podcini.util.MiscFormatter
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.graphics.ColorFilter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.OnLayoutChangeListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import coil.imageLoader
|
||||
import coil.request.ErrorResult
|
||||
import coil.request.ImageRequest
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
/**
|
||||
* Displays the description of a Playable object in a Webview.
|
||||
*/
|
||||
@UnstableApi
|
||||
class PlayerDetailsFragment : Fragment() {
|
||||
private lateinit var shownoteView: ShownotesWebView
|
||||
private var shownotesCleaner: ShownotesCleaner? = null
|
||||
|
||||
private var _binding: PlayerDetailsFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var prevItem: Episode? = null
|
||||
private var playable: Playable? = null
|
||||
private var currentItem: Episode? = null
|
||||
private var displayedChapterIndex = -1
|
||||
|
||||
private var cleanedNotes: String? = null
|
||||
|
||||
private var isLoading = false
|
||||
private var homeText: String? = null
|
||||
internal var showHomeText = false
|
||||
internal var readerhtml: String? = null
|
||||
|
||||
private val currentChapter: Chapter?
|
||||
get() {
|
||||
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
||||
return playable!!.getChapters()[displayedChapterIndex]
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
_binding = PlayerDetailsFragmentBinding.inflate(inflater)
|
||||
|
||||
val colorFilter: ColorFilter? = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(binding.txtvPodcastTitle.currentTextColor, BlendModeCompat.SRC_IN)
|
||||
binding.butNextChapter.colorFilter = colorFilter
|
||||
binding.butPrevChapter.colorFilter = colorFilter
|
||||
binding.chapterButton.setOnClickListener { ChaptersFragment().show(childFragmentManager, ChaptersFragment.TAG) }
|
||||
binding.butPrevChapter.setOnClickListener { seekToPrevChapter() }
|
||||
binding.butNextChapter.setOnClickListener { seekToNextChapter() }
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
shownoteView = binding.webview
|
||||
shownoteView.setTimecodeSelectedListener { time: Int -> seekTo(time) }
|
||||
shownoteView.setPageFinishedListener {
|
||||
// Restoring the scroll position might not always work
|
||||
shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
|
||||
}
|
||||
|
||||
binding.root.addOnLayoutChangeListener(object : OnLayoutChangeListener {
|
||||
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||
if (binding.root.measuredHeight != shownoteView.minimumHeight) shownoteView.setMinimumHeight(binding.root.measuredHeight)
|
||||
binding.root.removeOnLayoutChangeListener(this)
|
||||
}
|
||||
})
|
||||
registerForContextMenu(shownoteView)
|
||||
shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
return binding.root
|
||||
}
|
||||
|
||||
// override fun onStart() {
|
||||
// Logd(TAG, "onStart()")
|
||||
// super.onStart()
|
||||
// }
|
||||
|
||||
// override fun onStop() {
|
||||
// Logd(TAG, "onStop()")
|
||||
// super.onStop()
|
||||
// }
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
prevItem = null
|
||||
currentItem = null
|
||||
Logd(TAG, "Fragment destroyed")
|
||||
shownoteView.removeAllViews()
|
||||
shownoteView.destroy()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
return shownoteView.onContextItemSelected(item)
|
||||
}
|
||||
|
||||
internal fun updateInfo() {
|
||||
// if (isLoading) return
|
||||
lifecycleScope.launch {
|
||||
Logd(TAG, "in updateInfo")
|
||||
isLoading = true
|
||||
withContext(Dispatchers.IO) {
|
||||
if (currentItem == null) {
|
||||
playable = curMedia
|
||||
if (playable != null && playable is EpisodeMedia) {
|
||||
val episodeMedia = playable as EpisodeMedia
|
||||
currentItem = episodeMedia.episodeOrFetch()
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
}
|
||||
if (currentItem != null) {
|
||||
playable = currentItem!!.media
|
||||
if (prevItem?.identifier != currentItem!!.identifier) cleanedNotes = null
|
||||
if (cleanedNotes == null) {
|
||||
Logd(TAG, "calling load description ${currentItem!!.description==null} ${currentItem!!.title}")
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration()?:0)
|
||||
}
|
||||
prevItem = currentItem
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Logd(TAG, "subscribe: ${playable?.getEpisodeTitle()}")
|
||||
displayMediaInfo(playable!!)
|
||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
||||
Logd(TAG, "Webview loaded")
|
||||
}
|
||||
}.invokeOnCompletion { throwable ->
|
||||
isLoading = false
|
||||
if (throwable != null) Log.e(TAG, Log.getStackTraceString(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
fun buildHomeReaderText() {
|
||||
showHomeText = !showHomeText
|
||||
runOnIOScope {
|
||||
if (showHomeText) {
|
||||
homeText = currentItem!!.transcript
|
||||
if (homeText == null && currentItem?.link != null) {
|
||||
val url = currentItem!!.link!!
|
||||
val htmlSource = fetchHtmlSource(url)
|
||||
val readability4J = Readability4J(currentItem!!.link!!, htmlSource)
|
||||
val article = readability4J.parse()
|
||||
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||
if (!readerhtml.isNullOrEmpty()) {
|
||||
currentItem = upsertBlk(currentItem!!) {
|
||||
it.setTranscriptIfLonger(readerhtml)
|
||||
}
|
||||
homeText = currentItem!!.transcript
|
||||
// persistEpisode(currentItem)
|
||||
}
|
||||
}
|
||||
if (!homeText.isNullOrEmpty()) {
|
||||
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(homeText!!, 0)
|
||||
withContext(Dispatchers.Main) {
|
||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
||||
cleanedNotes ?: "No notes",
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
null)
|
||||
}
|
||||
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
||||
} else {
|
||||
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration() ?: 0)
|
||||
if (!cleanedNotes.isNullOrEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
||||
cleanedNotes ?: "No notes",
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
null)
|
||||
}
|
||||
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun displayMediaInfo(media: Playable) {
|
||||
Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}")
|
||||
val pubDateStr = MiscFormatter.formatAbbrev(context, media.getPubDate())
|
||||
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
|
||||
if (media is EpisodeMedia) {
|
||||
if (currentItem?.feedId != null) {
|
||||
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), currentItem!!.feedId!!)
|
||||
binding.txtvPodcastTitle.setOnClickListener { startActivity(openFeed) }
|
||||
}
|
||||
} else binding.txtvPodcastTitle.setOnClickListener(null)
|
||||
|
||||
binding.txtvPodcastTitle.setOnLongClickListener { copyText(media.getFeedTitle()) }
|
||||
binding.episodeDate.text = StringUtils.stripToEmpty(pubDateStr)
|
||||
binding.txtvEpisodeTitle.text = currentItem?.title
|
||||
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(currentItem?.title?:"") }
|
||||
binding.txtvEpisodeTitle.setOnClickListener {
|
||||
val lines = binding.txtvEpisodeTitle.lineCount
|
||||
val animUnit = 1500
|
||||
if (lines > binding.txtvEpisodeTitle.maxLines) {
|
||||
val titleHeight = (binding.txtvEpisodeTitle.height - binding.txtvEpisodeTitle.paddingTop - binding.txtvEpisodeTitle.paddingBottom)
|
||||
val verticalMarquee: ObjectAnimator = ObjectAnimator.ofInt(binding.txtvEpisodeTitle, "scrollY", 0,
|
||||
(lines - binding.txtvEpisodeTitle.maxLines) * (titleHeight / binding.txtvEpisodeTitle.maxLines)).setDuration((lines * animUnit).toLong())
|
||||
val fadeOut: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 0f)
|
||||
fadeOut.setStartDelay(animUnit.toLong())
|
||||
fadeOut.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.txtvEpisodeTitle.scrollTo(0, 0)
|
||||
}
|
||||
})
|
||||
val fadeBackIn: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 1f)
|
||||
val set = AnimatorSet()
|
||||
set.playSequentially(verticalMarquee, fadeOut, fadeBackIn)
|
||||
set.start()
|
||||
}
|
||||
}
|
||||
displayedChapterIndex = -1
|
||||
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())) //calls displayCoverImage
|
||||
updateChapterControlVisibility()
|
||||
}
|
||||
|
||||
private fun updateChapterControlVisibility() {
|
||||
var chapterControlVisible = false
|
||||
when {
|
||||
playable?.getChapters() != null -> chapterControlVisible = playable!!.getChapters().isNotEmpty()
|
||||
playable is EpisodeMedia -> {
|
||||
val item_ = (playable as EpisodeMedia).episodeOrFetch()
|
||||
// If an item has chapters but they are not loaded yet, still display the button.
|
||||
chapterControlVisible = !item_?.chapters.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
val newVisibility = if (chapterControlVisible) View.VISIBLE else View.GONE
|
||||
if (binding.chapterButton.visibility != newVisibility) {
|
||||
binding.chapterButton.visibility = newVisibility
|
||||
ObjectAnimator.ofFloat(binding.chapterButton, "alpha",
|
||||
(if (chapterControlVisible) 0 else 1).toFloat(), (if (chapterControlVisible) 1 else 0).toFloat()).start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshChapterData(chapterIndex: Int) {
|
||||
Logd(TAG, "in refreshChapterData $chapterIndex")
|
||||
if (playable != null && chapterIndex > -1) {
|
||||
if (playable!!.getPosition() > playable!!.getDuration() || chapterIndex >= playable!!.getChapters().size - 1) {
|
||||
displayedChapterIndex = playable!!.getChapters().size - 1
|
||||
binding.butNextChapter.visibility = View.INVISIBLE
|
||||
} else {
|
||||
displayedChapterIndex = chapterIndex
|
||||
binding.butNextChapter.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
displayCoverImage()
|
||||
}
|
||||
|
||||
private fun displayCoverImage() {
|
||||
if (playable == null) return
|
||||
if (displayedChapterIndex == -1 || playable!!.getChapters().isEmpty() || playable!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
|
||||
val imageLoader = binding.imgvCover.context.imageLoader
|
||||
val imageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(playable!!.getImageLocation())
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.placeholder(R.color.light_gray)
|
||||
.listener(object : ImageRequest.Listener {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.error(R.mipmap.ic_launcher)
|
||||
.target(binding.imgvCover)
|
||||
.build()
|
||||
imageLoader.enqueue(fallbackImageRequest)
|
||||
}
|
||||
})
|
||||
.target(binding.imgvCover)
|
||||
.build()
|
||||
imageLoader.enqueue(imageRequest)
|
||||
|
||||
} else {
|
||||
val imgLoc = EmbeddedChapterImage.getModelFor(playable!!, displayedChapterIndex)
|
||||
val imageLoader = binding.imgvCover.context.imageLoader
|
||||
val imageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(imgLoc)
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.placeholder(R.color.light_gray)
|
||||
.listener(object : ImageRequest.Listener {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.error(R.mipmap.ic_launcher)
|
||||
.target(binding.imgvCover)
|
||||
.build()
|
||||
imageLoader.enqueue(fallbackImageRequest)
|
||||
}
|
||||
})
|
||||
.target(binding.imgvCover)
|
||||
.build()
|
||||
imageLoader.enqueue(imageRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun seekToPrevChapter() {
|
||||
val curr: Chapter? = currentChapter
|
||||
if (curr == null || displayedChapterIndex == -1) return
|
||||
|
||||
when {
|
||||
displayedChapterIndex < 1 -> seekTo(0)
|
||||
(curPositionFB - 10000 * curSpeedFB) < curr.start -> {
|
||||
refreshChapterData(displayedChapterIndex - 1)
|
||||
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||
}
|
||||
else -> seekTo(curr.start.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun seekToNextChapter() {
|
||||
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= playable!!.getChapters().size) return
|
||||
refreshChapterData(displayedChapterIndex + 1)
|
||||
seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||
}
|
||||
|
||||
|
||||
@UnstableApi override fun onPause() {
|
||||
super.onPause()
|
||||
savePreference()
|
||||
}
|
||||
|
||||
@UnstableApi private fun savePreference() {
|
||||
Logd(TAG, "Saving preferences")
|
||||
val editor = prefs?.edit() ?: return
|
||||
if (curMedia != null) {
|
||||
Logd(TAG, "Saving scroll position: " + binding.itemDescriptionFragment.scrollY)
|
||||
editor.putInt(PREF_SCROLL_Y, binding.itemDescriptionFragment.scrollY)
|
||||
editor.putString(PREF_PLAYABLE_ID, curMedia!!.getIdentifier().toString())
|
||||
} else {
|
||||
Logd(TAG, "savePreferences was called while media or webview was null")
|
||||
editor.putInt(PREF_SCROLL_Y, -1)
|
||||
editor.putString(PREF_PLAYABLE_ID, "")
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
@UnstableApi private fun restoreFromPreference(): Boolean {
|
||||
if ((activity as MainActivity).bottomSheet.state != BottomSheetBehavior.STATE_EXPANDED) return false
|
||||
|
||||
Logd(TAG, "Restoring from preferences")
|
||||
val activity: Activity? = activity
|
||||
if (activity != null) {
|
||||
val id = prefs!!.getString(PREF_PLAYABLE_ID, "")
|
||||
val scrollY = prefs!!.getInt(PREF_SCROLL_Y, -1)
|
||||
if (scrollY != -1) {
|
||||
if (id == curMedia?.getIdentifier()?.toString()) {
|
||||
Logd(TAG, "Restored scroll Position: $scrollY")
|
||||
binding.itemDescriptionFragment.scrollTo(binding.itemDescriptionFragment.scrollX, scrollY)
|
||||
return true
|
||||
}
|
||||
Logd(TAG, "reset scroll Position: 0")
|
||||
binding.itemDescriptionFragment.scrollTo(0, 0)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun scrollToTop() {
|
||||
binding.itemDescriptionFragment.scrollTo(0, 0)
|
||||
savePreference()
|
||||
}
|
||||
|
||||
fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||
if (playable?.getIdentifier() != event.media?.getIdentifier()) return
|
||||
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(playable, event.position)
|
||||
if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) refreshChapterData(newChapterIndex)
|
||||
}
|
||||
|
||||
fun setItem(item_: Episode) {
|
||||
Logd(TAG, "setItem ${item_.title}")
|
||||
if (currentItem?.identifier != item_.identifier) {
|
||||
currentItem = item_
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
}
|
||||
|
||||
// override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
// super.onConfigurationChanged(newConfig)
|
||||
// configureForOrientation(newConfig)
|
||||
// }
|
||||
//
|
||||
// private fun configureForOrientation(newConfig: Configuration) {
|
||||
// val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
//
|
||||
//// binding.coverFragment.orientation = if (isPortrait) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
|
||||
//
|
||||
// if (isPortrait) {
|
||||
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
// } else {
|
||||
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||
// }
|
||||
//
|
||||
// (binding.episodeDetails.parent as ViewGroup).removeView(binding.episodeDetails)
|
||||
// if (isPortrait) {
|
||||
// binding.coverFragment.addView(binding.episodeDetails)
|
||||
// } else {
|
||||
// binding.coverFragmentTextContainer.addView(binding.episodeDetails)
|
||||
// }
|
||||
// }
|
||||
|
||||
@UnstableApi private fun copyText(text: String): Boolean {
|
||||
val clipboardManager: ClipboardManager? = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java)
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Podcini", text))
|
||||
if (Build.VERSION.SDK_INT <= 32) {
|
||||
(requireActivity() as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = PlayerDetailsFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
private const val PREF = "ItemDescriptionFragmentPrefs"
|
||||
private const val PREF_SCROLL_Y = "prefScrollY"
|
||||
private const val PREF_PLAYABLE_ID = "prefPlayableId"
|
||||
|
||||
var prefs: SharedPreferences? = null
|
||||
fun getSharedPrefs(context: Context) {
|
||||
if (prefs == null) prefs = context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,12 @@
|
|||
# 6.5.9
|
||||
|
||||
* partially fixed an issue seen on Samsung Android 14 device where after playing the user-started episode, subsequent episode in the queue is not played in foreground service and there is not notification panel and can get stopped by the system.
|
||||
* the current fix is though the subsequent episodes are still played without notification, the play is not stopped by the system.
|
||||
* if videoMode of a feed is set to "audio only",
|
||||
* press on icon in the player UI will expand the player detailed view (rather than video view)
|
||||
* "show video" on the menu of AudioPlayer view is hidden
|
||||
* some class restructuring
|
||||
|
||||
# 6.5.8
|
||||
|
||||
* corrected mis-behavior of speed settings for video media
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
Version 6.5.9 brings several changes:
|
||||
|
||||
* partially fixed an issue seen on Samsung Android 14 device where after playing the user-started episode, subsequent episode in the queue is not played in foreground service and there is not notification panel and can get stopped by the system.
|
||||
* the current fix is though the subsequent episodes are still played without notification, the play is not stopped by the system.
|
||||
* if videoMode of a feed is set to "audio only",
|
||||
* press on icon in the player UI will expand the player detailed view (rather than video view)
|
||||
* "show video" on the menu of AudioPlayer view is hidden
|
||||
* some class restructuring
|
|
@ -7,5 +7,5 @@ android.nonFinalResIds=true
|
|||
org.gradle.caching=true
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
kotlin.daemon.jvmargs=-Xmx1g
|
||||
org.gradle.configuration-cache=true
|
||||
|
|
Loading…
Reference in New Issue