4.3.3 release

This commit is contained in:
Xilin Jia 2024-03-24 20:36:12 +00:00
parent 64e3e64d21
commit 5cb3b20b00
30 changed files with 543 additions and 969 deletions

View File

@ -3,7 +3,7 @@
<img width="100" src="https://raw.githubusercontent.com/xilinjia/podcini/main/images/icon 256x256.png" align="left" style="margin-right:15px"/>
Podcini is an open source podcast manager/player project.
This app is a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
This project is a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
Compared to AntennaPod this project:
1. Migrated the media player to `androidx.media3`,
@ -11,7 +11,7 @@ Compared to AntennaPod this project:
3. Relies on the most recent dependencies,
4. Is __purely__ Kotlin based,
4. Targets Android 14,
5. Aims to be as effective as possible. <!-- NOTE: wording of this can be improved -->
5. Aims to improve efficiency and provide more user-friendly features
## Version 4

View File

@ -149,8 +149,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020113
versionName "4.3.2"
versionCode 3020114
versionName "4.3.3"
def commit = ""
try {

View File

@ -37,7 +37,7 @@ class BugReportActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
supportActionBar!!.setDisplayShowHomeEnabled(true)
_binding = BugReportBinding.inflate(layoutInflater)
setContentView(R.layout.bug_report)
setContentView(binding.root)
var stacktrace = "No crash report recorded"
try {

View File

@ -104,7 +104,8 @@ class MainActivity : CastEnabledActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
_binding = MainActivityBinding.inflate(layoutInflater)
setContentView(R.layout.main_activity)
// setContentView(R.layout.main_activity)
setContentView(binding.root)
recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25)
dummyView = object : View(this) {}
@ -162,6 +163,7 @@ class MainActivity : CastEnabledActivity() {
checkFirstLaunch()
this.bottomSheet = BottomSheetBehavior.from(audioPlayerFragmentView) as LockableBottomSheetBehavior<*>
this.bottomSheet.isHideable = false
this.bottomSheet.isDraggable = false
this.bottomSheet.setBottomSheetCallback(bottomSheetCallback)
restartUpdateAlarm(this, false)
@ -272,18 +274,22 @@ class MainActivity : CastEnabledActivity() {
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
override fun onStateChanged(view: View, state: Int) {
if (state == BottomSheetBehavior.STATE_COLLAPSED) {
onSlide(view,0.0f)
} else if (state == BottomSheetBehavior.STATE_EXPANDED) {
onSlide(view, 1.0f)
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> {
onSlide(view,0.0f)
}
BottomSheetBehavior.STATE_EXPANDED -> {
onSlide(view, 1.0f)
}
else -> {}
}
}
override fun onSlide(view: View, slideOffset: Float) {
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
if (slideOffset == 0.0f) { //STATE_COLLAPSED
audioPlayer.scrollToPage(AudioPlayerFragment.FIRST_PAGE)
}
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
// audioPlayer.scrollToPage(AudioPlayerFragment.FIRST_PAGE)
// }
audioPlayer.fadePlayerToToolbar(slideOffset)
}
}

View File

@ -101,7 +101,6 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
controller = newPlaybackController()
controller!!.init()
loadMediaInfo()
// EventBus.getDefault().register(this)
}
@UnstableApi
@ -124,16 +123,10 @@ class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
controller?.release()
controller = null // prevent leak
disposable?.dispose()
// EventBus.getDefault().unregister(this)
}
@UnstableApi
override fun onStop() {
// controller?.release()
// controller = null // prevent leak
// disposable?.dispose()
EventBus.getDefault().unregister(this)
super.onStop()
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {

View File

@ -19,13 +19,15 @@ import ac.mdiq.podcini.receiver.PlayerWidget
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
class WidgetConfigActivity : AppCompatActivity() {
private var _binding: ActivityWidgetConfigBinding? = null
private val binding get() = _binding!!
private var _wpBinding: PlayerWidgetBinding? = null
private val wpBinding get() = _wpBinding!!
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private lateinit var widgetPreview: View
private lateinit var wpBinding: PlayerWidgetBinding
private lateinit var opacitySeekBar: SeekBar
private lateinit var opacityTextView: TextView
private lateinit var ckPlaybackSpeed: CheckBox
@ -37,7 +39,7 @@ class WidgetConfigActivity : AppCompatActivity() {
setTheme(getTheme(this))
super.onCreate(savedInstanceState)
_binding = ActivityWidgetConfigBinding.inflate(layoutInflater)
setContentView(R.layout.activity_widget_config)
setContentView(binding.root)
val configIntent = intent
val extras = configIntent.extras
@ -55,10 +57,10 @@ class WidgetConfigActivity : AppCompatActivity() {
opacityTextView = binding.widgetOpacityTextView
opacitySeekBar = binding.widgetOpacitySeekBar
widgetPreview = binding.widgetConfigPreview.widgetLayout
wpBinding = PlayerWidgetBinding.bind(widgetPreview)
widgetPreview = binding.widgetConfigPreview.playerWidget
_wpBinding = PlayerWidgetBinding.bind(widgetPreview)
binding.butConfirm.setOnClickListener { confirmCreateWidget() }
binding.butConfirm.setOnClickListener{ confirmCreateWidget() }
opacitySeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
opacityTextView.text = seekBar.progress.toString() + "%"
@ -96,7 +98,9 @@ class WidgetConfigActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
_binding = null
_wpBinding = null
}
private fun setInitialState() {
val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
ckPlaybackSpeed.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, false)

View File

@ -1,8 +1,14 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.FilterDialogBinding
import ac.mdiq.podcini.databinding.FilterDialogRowBinding
import ac.mdiq.podcini.feed.FeedItemFilterGroup
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -13,13 +19,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButtonToggleGroup
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.FilterDialogBinding
import ac.mdiq.podcini.feed.FeedItemFilterGroup
import ac.mdiq.podcini.databinding.FilterDialogRowBinding
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.ui.fragment.ItemDescriptionFragment
import android.util.Log
abstract class ItemFilterDialog : BottomSheetDialogFragment() {
private lateinit var rows: LinearLayout

View File

@ -1,10 +1,38 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.event.*
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.model.feed.Chapter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView
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.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.ui.view.ChapterSeekBar
import ac.mdiq.podcini.ui.view.PlayButton
import ac.mdiq.podcini.util.ChapterUtils
import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.TimeSpeedConverter
import ac.mdiq.podcini.util.event.FavoritesEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.util.Log
import android.view.*
import android.widget.ImageButton
@ -13,40 +41,13 @@ import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.cardview.widget.CardView
import androidx.core.app.ShareCompat
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.media3.common.util.UnstableApi
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.elevation.SurfaceColors
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.util.ChapterUtils
import ac.mdiq.podcini.util.TimeSpeedConverter
import ac.mdiq.podcini.playback.PlaybackController
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.util.event.FavoritesEvent
import ac.mdiq.podcini.ui.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.storage.model.feed.Chapter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.event.*
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView
import ac.mdiq.podcini.ui.view.ChapterSeekBar
import ac.mdiq.podcini.ui.view.PlayButton
import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.event.PlayerErrorEvent
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import io.reactivex.Maybe
import io.reactivex.MaybeEmitter
import io.reactivex.android.schedulers.AndroidSchedulers
@ -72,7 +73,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
lateinit var txtvPlaybackSpeed: TextView
private lateinit var episodeTitle: TextView
private lateinit var pager: ViewPager2
private lateinit var itemDesrView: View
private lateinit var txtvPosition: TextView
private lateinit var txtvLength: TextView
private lateinit var sbPosition: ChapterSeekBar
@ -110,7 +111,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
toolbar = binding.toolbar
toolbar.title = ""
toolbar.setNavigationOnClickListener {
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
val bottomSheet = (activity as MainActivity).bottomSheet
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED)
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
else bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
}
toolbar.setOnMenuItemClickListener(this)
@ -123,6 +127,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
playerFragment.setBackgroundColor(
SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
itemDesrView = binding.itemDescription
episodeTitle = binding.titleView
butPlaybackSpeed = binding.butPlaybackSpeed
txtvPlaybackSpeed = binding.txtvPlaybackSpeed
@ -146,20 +151,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
sbPosition.setOnSeekBarChangeListener(this)
pager = binding.pager
pager.adapter = AudioPlayerPagerAdapter(this@AudioPlayerFragment)
// Required for getChildAt(int) in ViewPagerBottomSheetBehavior to return the correct page
pager.offscreenPageLimit = NUM_CONTENT_FRAGMENTS
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
pager.post {
if (activity != null) {
// By the time this is posted, the activity might be closed again.
(activity as MainActivity).bottomSheet.updateScrollingChild()
}
}
}
})
val fm = requireActivity().supportFragmentManager
val transaction = fm.beginTransaction()
val itemDescFrag = PlayerDetailsFragment()
transaction.replace(R.id.itemDescription, itemDescFrag).commit()
controller = newPlaybackController()
controller?.init()
@ -474,26 +469,45 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
(activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
val media: Playable = controller?.getMedia() ?: return false
val feedItem: FeedItem? = if ((media is FeedMedia)) media.item else null
if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, item.itemId, feedItem)) {
val feedItem: FeedItem? = if (media is FeedMedia) media.item else null
if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) {
return true
}
val itemId = item.itemId
if (itemId == R.id.disable_sleeptimer_item || itemId == R.id.set_sleeptimer_item) {
SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
return true
} else if (itemId == R.id.open_feed_item) {
if (feedItem != null) {
val intent: Intent = MainActivity.getIntentToOpenFeed(requireContext(), feedItem.feedId)
startActivity(intent)
val itemId = menuItem.itemId
when (itemId) {
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> {
SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
return true
}
return true
R.id.open_feed_item -> {
if (feedItem != null) {
val intent: Intent = MainActivity.getIntentToOpenFeed(requireContext(), feedItem.feedId)
startActivity(intent)
}
return true
}
R.id.share_notes -> {
if (feedItem == null) return false
val notes = feedItem.description
if (!notes.isNullOrEmpty()) {
val shareText = if (Build.VERSION.SDK_INT >= 24) Html.fromHtml(notes, Html.FROM_HTML_MODE_LEGACY).toString()
else Html.fromHtml(notes).toString()
val context = requireContext()
val intent = ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(shareText)
.setChooserTitle(R.string.share_notes_label)
.createChooserIntent()
context.startActivity(intent)
}
return true
}
else -> return false
}
return false
}
fun fadePlayerToToolbar(slideOffset: Float) {
@ -506,39 +520,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
toolbar.visibility = if (toolbarFadeProgress < 0.01f) View.INVISIBLE else View.VISIBLE
}
private class AudioPlayerPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun createFragment(position: Int): Fragment {
Log.d(TAG, "getItem($position)")
return when (position) {
FIRST_PAGE -> ItemDescriptionFragment()
SECOND_PAGE -> CoverFragment()
else -> ItemDescriptionFragment()
}
}
override fun getItemCount(): Int {
return NUM_CONTENT_FRAGMENTS
}
companion object {
private const val TAG = "AudioPlayerPagerAdapter"
}
}
@JvmOverloads
fun scrollToPage(page: Int, smoothScroll: Boolean = false) {
pager.setCurrentItem(page, smoothScroll)
val visibleChild = childFragmentManager.findFragmentByTag("f$FIRST_PAGE")
if (visibleChild is ItemDescriptionFragment) {
visibleChild.scrollToTop()
}
}
companion object {
const val TAG: String = "AudioPlayerFragment"
const val FIRST_PAGE: Int = 0
const val SECOND_PAGE: Int = 1
private const val NUM_CONTENT_FRAGMENTS = 2
}
}

View File

@ -220,10 +220,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@UnstableApi override fun onMenuItemClick(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.open_podcast -> {
openPodcast()
return true
}
R.id.share_notes -> {
if (item == null) return false
val notes = item!!.description
@ -279,11 +275,11 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
return
}
if (item!!.hasMedia()) {
FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item)
FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.open_podcast)
} else {
// these are already available via button1 and button2
FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item,
R.id.mark_read_item, R.id.visit_website_item)
R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
}
if (item!!.feed != null) txtvPodcast.text = item!!.feed!!.title
txtvTitle.text = item!!.title
@ -337,14 +333,19 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
}
if (item != null) {
actionButton1 = if (PlaybackStatus.isCurrentlyPlaying(media)) {
PauseActionButton(item!!)
} else if (item!!.feed != null && item!!.feed!!.isLocalFeed) {
PlayLocalActionButton(item)
} else if (media.isDownloaded()) {
PlayActionButton(item!!)
} else {
StreamActionButton(item!!)
actionButton1 = when {
PlaybackStatus.isCurrentlyPlaying(media) -> {
PauseActionButton(item!!)
}
item!!.feed != null && item!!.feed!!.isLocalFeed -> {
PlayLocalActionButton(item)
}
media.isDownloaded() -> {
PlayActionButton(item!!)
}
else -> {
StreamActionButton(item!!)
}
}
actionButton2 = if (dls != null && media.download_url != null && dls.isDownloadingEpisode(media.download_url!!)) {
CancelDownloadActionButton(item!!)

View File

@ -1,173 +0,0 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.databinding.ItemDescriptionFragmentBinding
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.ui.gui.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import android.app.Activity
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 androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import io.reactivex.Maybe
import io.reactivex.MaybeEmitter
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
/**
* Displays the description of a Playable object in a Webview.
*/
@UnstableApi
class ItemDescriptionFragment : Fragment() {
private lateinit var webvDescription: ShownotesWebView
private var _binding: ItemDescriptionFragmentBinding? = null
private val binding get() = _binding!!
private var webViewLoader: Disposable? = null
private var controller: PlaybackController? = null
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Log.d(TAG, "fragment onCreateView")
_binding = ItemDescriptionFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
webvDescription = binding.webview
webvDescription.setTimecodeSelectedListener { time: Int? ->
controller?.seekTo(time!!)
}
webvDescription.setPageFinishedListener {
// Restoring the scroll position might not always work
webvDescription.postDelayed({ this@ItemDescriptionFragment.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 != webvDescription.minimumHeight) {
webvDescription.setMinimumHeight(binding.root.measuredHeight)
}
binding.root.removeOnLayoutChangeListener(this)
}
})
registerForContextMenu(webvDescription)
controller = object : PlaybackController(requireActivity()) {
override fun loadMediaInfo() {
load()
}
}
controller?.init()
load()
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
controller?.release()
controller = null
Log.d(TAG, "Fragment destroyed")
webvDescription.removeAllViews()
webvDescription.destroy()
}
override fun onContextItemSelected(item: MenuItem): Boolean {
return webvDescription.onContextItemSelected(item)
}
@UnstableApi private fun load() {
Log.d(TAG, "load() called")
webViewLoader?.dispose()
val context = context ?: return
webViewLoader = Maybe.create { emitter: MaybeEmitter<String?> ->
val media = controller?.getMedia()
if (media == null) {
emitter.onComplete()
return@create
}
if (media is FeedMedia) {
var item = media.item
if (item == null) {
item = DBReader.getFeedItem(media.itemId)
media.setItem(item)
}
if (item != null && item.description == null) DBReader.loadDescriptionOfFeedItem(item)
}
val shownotesCleaner = ShownotesCleaner(context, media.getDescription()?:"", media.getDuration())
emitter.onSuccess(shownotesCleaner.processShownotes())
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ data: String? ->
webvDescription.loadDataWithBaseURL("https://127.0.0.1", data!!, "text/html",
"utf-8", "about:blank")
Log.d(TAG, "Webview loaded")
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
}
@UnstableApi override fun onPause() {
super.onPause()
savePreference()
}
@UnstableApi private fun savePreference() {
Log.d(TAG, "Saving preferences")
val prefs = requireActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE)
val editor = prefs.edit()
if (controller?.getMedia() != null) {
Log.d(TAG, "Saving scroll position: " + webvDescription.scrollY)
editor.putInt(PREF_SCROLL_Y, webvDescription.scrollY)
editor.putString(PREF_PLAYABLE_ID, controller!!.getMedia()!!.getIdentifier().toString())
} else {
Log.d(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 {
Log.d(TAG, "Restoring from preferences")
val activity: Activity? = activity
if (activity != null) {
val prefs = activity.getSharedPreferences(PREF, Activity.MODE_PRIVATE)
val id = prefs.getString(PREF_PLAYABLE_ID, "")
val scrollY = prefs.getInt(PREF_SCROLL_Y, -1)
if (controller != null && scrollY != -1 && controller!!.getMedia() != null && id == controller!!.getMedia()!!.getIdentifier().toString()) {
Log.d(TAG, "Restored scroll Position: $scrollY")
webvDescription.scrollTo(webvDescription.scrollX, scrollY)
return true
}
}
return false
}
fun scrollToTop() {
webvDescription.scrollTo(0, 0)
savePreference()
}
override fun onStop() {
super.onStop()
webViewLoader?.dispose()
}
companion object {
private const val TAG = "ItemDescriptionFragment"
private const val PREF = "ItemDescriptionFragmentPrefs"
private const val PREF_SCROLL_Y = "prefScrollY"
private const val PREF_PLAYABLE_ID = "prefPlayableId"
}
}

View File

@ -1,34 +1,37 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
import ac.mdiq.podcini.feed.util.ImageResourceUtils
import ac.mdiq.podcini.util.ChapterUtils
import ac.mdiq.podcini.util.DateFormatter
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.databinding.CoverFragmentBinding
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.model.feed.Chapter
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.gui.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.ChapterUtils
import ac.mdiq.podcini.util.DateFormatter
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.content.res.Configuration
import android.graphics.ColorFilter
import android.graphics.drawable.Drawable
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.LinearLayout
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
@ -46,36 +49,36 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.apache.commons.lang3.StringUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
/**
* Displays the cover and the title of a FeedItem.
* Displays the description of a Playable object in a Webview.
*/
class CoverFragment : Fragment() {
private var _binding: CoverFragmentBinding? = null
@UnstableApi
class PlayerDetailsFragment : Fragment() {
private lateinit var webvDescription: ShownotesWebView
private var _binding: PlayerDetailsFragmentBinding? = null
private val binding get() = _binding!!
private var controller: PlaybackController? = null
private var disposable: Disposable? = null
private var displayedChapterIndex = -1
private var media: Playable? = null
private var displayedChapterIndex = -1
private var disposable: Disposable? = null
private var webViewLoader: Disposable? = null
private var controller: PlaybackController? = null
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Log.d(TAG, "fragment onCreateView")
_binding = CoverFragmentBinding.inflate(inflater)
_binding = PlayerDetailsFragmentBinding.inflate(inflater)
binding.imgvCover.setOnClickListener { onPlayPause() }
binding.openDescription.setOnClickListener {
(requireParentFragment() as AudioPlayerFragment)
.scrollToPage(AudioPlayerFragment.FIRST_PAGE, true)
}
val colorFilter: ColorFilter? = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
binding.txtvPodcastTitle.currentTextColor, BlendModeCompat.SRC_IN)
binding.butNextChapter.colorFilter = colorFilter
binding.butPrevChapter.colorFilter = colorFilter
binding.descriptionIcon.colorFilter = colorFilter
binding.chapterButton.setOnClickListener {
ChaptersFragment().show(
childFragmentManager, ChaptersFragment.TAG)
@ -83,41 +86,88 @@ class CoverFragment : Fragment() {
binding.butPrevChapter.setOnClickListener { seekToPrevChapter() }
binding.butNextChapter.setOnClickListener { seekToNextChapter() }
Log.d(TAG, "fragment onCreateView")
webvDescription = binding.webview
webvDescription.setTimecodeSelectedListener { time: Int? ->
controller?.seekTo(time!!)
}
webvDescription.setPageFinishedListener {
// Restoring the scroll position might not always work
webvDescription.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 != webvDescription.minimumHeight) {
webvDescription.setMinimumHeight(binding.root.measuredHeight)
}
binding.root.removeOnLayoutChangeListener(this)
}
})
registerForContextMenu(webvDescription)
controller = object : PlaybackController(requireActivity()) {
override fun loadMediaInfo() {
this@CoverFragment.loadMediaInfo(false)
load()
loadMediaInfo(false)
}
}
controller?.init()
load()
loadMediaInfo(false)
EventBus.getDefault().register(this)
return binding.root
}
@OptIn(UnstableApi::class) override fun onDestroyView() {
override fun onDestroyView() {
super.onDestroyView()
_binding = null
controller?.release()
controller = null
EventBus.getDefault().unregister(this)
Log.d(TAG, "Fragment destroyed")
webvDescription.removeAllViews()
webvDescription.destroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
configureForOrientation(resources.configuration)
override fun onContextItemSelected(item: MenuItem): Boolean {
return webvDescription.onContextItemSelected(item)
}
@UnstableApi private fun load() {
Log.d(TAG, "load() called")
webViewLoader?.dispose()
val context = context ?: return
webViewLoader = Maybe.create { emitter: MaybeEmitter<String?> ->
media = controller?.getMedia()
if (media == null) {
emitter.onComplete()
return@create
}
if (media is FeedMedia) {
val feedMedia = media as FeedMedia
val item = feedMedia.item
if (item != null && item.description == null) DBReader.loadDescriptionOfFeedItem(item)
}
val shownotesCleaner = ShownotesCleaner(context, media!!.getDescription()?:"", media!!.getDuration())
emitter.onSuccess(shownotesCleaner.processShownotes())
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ data: String? ->
webvDescription.loadDataWithBaseURL("https://127.0.0.1", data!!, "text/html",
"utf-8", "about:blank")
Log.d(TAG, "Webview loaded")
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
}
@UnstableApi private fun loadMediaInfo(includingChapters: Boolean) {
disposable?.dispose()
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
val media: Playable? = controller?.getMedia()
media = controller?.getMedia()
if (media != null) {
if (includingChapters) {
ChapterUtils.loadChapters(media, requireContext(), false)
}
emitter.onSuccess(media)
emitter.onSuccess(media!!)
} else {
emitter.onComplete()
}
@ -126,19 +176,12 @@ class CoverFragment : Fragment() {
.subscribe({ media: Playable ->
this.media = media
displayMediaInfo(media)
if (!includingChapters) {
loadMediaInfo(true)
}
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
}
@UnstableApi private fun displayMediaInfo(media: Playable) {
val pubDateStr = DateFormatter.formatAbbrev(context, media.getPubDate())
binding.txtvPodcastTitle.text = (StringUtils.stripToEmpty(media.getFeedTitle())
+ "\u00A0"
+ ""
+ "\u00A0"
+ StringUtils.replace(StringUtils.stripToEmpty(pubDateStr), " ", "\u00A0"))
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
if (media is FeedMedia) {
val items = media.item
if (items != null) {
@ -149,6 +192,7 @@ class CoverFragment : Fragment() {
binding.txtvPodcastTitle.setOnClickListener(null)
}
binding.txtvPodcastTitle.setOnLongClickListener { copyText(media.getFeedTitle()) }
binding.episodeDate.text = StringUtils.stripToEmpty(pubDateStr)
binding.txtvEpisodeTitle.text = media.getEpisodeTitle()
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(media.getEpisodeTitle()) }
binding.txtvEpisodeTitle.setOnClickListener {
@ -217,6 +261,36 @@ class CoverFragment : Fragment() {
displayCoverImage()
}
private fun displayCoverImage() {
if (media == null) return
val options: RequestOptions = RequestOptions()
.dontAnimate()
.transform(FitCenter(),
RoundedCorners((16 * resources.displayMetrics.density).toInt()))
val cover: RequestBuilder<Drawable> = Glide.with(this)
.load(media!!.getImageLocation())
.error(Glide.with(this)
.load(ImageResourceUtils.getFallbackImageLocation(media!!))
.apply(options))
.apply(options)
if (displayedChapterIndex == -1 || media!!.getChapters().isEmpty() || media!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
cover.into(binding.imgvCover)
} else {
Glide.with(this)
.load(ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage.getModelFor(media!!, displayedChapterIndex))
.apply(options)
.thumbnail(cover)
.error(cover)
.into(binding.imgvCover)
}
}
@UnstableApi fun onPlayPause() {
controller?.playPause()
}
private val currentChapter: Chapter?
get() {
if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1) {
@ -251,13 +325,47 @@ class CoverFragment : Fragment() {
controller!!.seekTo(media!!.getChapters()[displayedChapterIndex].start.toInt())
}
@UnstableApi override fun onStart() {
super.onStart()
@UnstableApi override fun onPause() {
super.onPause()
savePreference()
}
@UnstableApi override fun onStop() {
super.onStop()
disposable?.dispose()
@UnstableApi private fun savePreference() {
Log.d(TAG, "Saving preferences")
val prefs = requireActivity().getSharedPreferences(PREF, Activity.MODE_PRIVATE)
val editor = prefs.edit()
if (controller?.getMedia() != null) {
Log.d(TAG, "Saving scroll position: " + webvDescription.scrollY)
editor.putInt(PREF_SCROLL_Y, webvDescription.scrollY)
editor.putString(PREF_PLAYABLE_ID, controller!!.getMedia()!!.getIdentifier().toString())
} else {
Log.d(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 {
Log.d(TAG, "Restoring from preferences")
val activity: Activity? = activity
if (activity != null) {
val prefs = activity.getSharedPreferences(PREF, Activity.MODE_PRIVATE)
val id = prefs.getString(PREF_PLAYABLE_ID, "")
val scrollY = prefs.getInt(PREF_SCROLL_Y, -1)
if (controller != null && scrollY != -1 && controller!!.getMedia() != null && id == controller!!.getMedia()!!.getIdentifier().toString()) {
Log.d(TAG, "Restored scroll Position: $scrollY")
webvDescription.scrollTo(webvDescription.scrollX, scrollY)
return true
}
}
return false
}
fun scrollToTop() {
webvDescription.scrollTo(0, 0)
savePreference()
}
@Subscribe(threadMode = ThreadMode.MAIN)
@ -268,64 +376,39 @@ class CoverFragment : Fragment() {
}
}
private fun displayCoverImage() {
if (media == null) return
val options: RequestOptions = RequestOptions()
.dontAnimate()
.transform(FitCenter(),
RoundedCorners((16 * resources.displayMetrics.density).toInt()))
// 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)
// }
// }
val cover: RequestBuilder<Drawable> = Glide.with(this)
.load(media!!.getImageLocation())
.error(Glide.with(this)
.load(ImageResourceUtils.getFallbackImageLocation(media!!))
.apply(options))
.apply(options)
if (displayedChapterIndex == -1 || media!!.getChapters().isEmpty() || media!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
cover.into(binding.imgvCover)
} else {
Glide.with(this)
.load(ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage.getModelFor(media!!, displayedChapterIndex))
.apply(options)
.thumbnail(cover)
.error(cover)
.into(binding.imgvCover)
}
}
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 fun onPlayPause() {
controller?.playPause()
override fun onStop() {
super.onStop()
webViewLoader?.dispose()
}
@UnstableApi private fun copyText(text: String): Boolean {
@ -339,6 +422,10 @@ class CoverFragment : Fragment() {
}
companion object {
private const val TAG = "CoverFragment"
private const val TAG = "ItemDescriptionFragment"
private const val PREF = "ItemDescriptionFragmentPrefs"
private const val PREF_SCROLL_Y = "prefScrollY"
private const val PREF_PLAYABLE_ID = "prefPlayableId"
}
}

View File

@ -42,9 +42,8 @@ object FeedItemMenuHandler {
*/
@UnstableApi
fun onPrepareMenu(menu: Menu?, selectedItem: FeedItem?): Boolean {
if (menu == null || selectedItem == null) {
return false
}
if (menu == null || selectedItem == null) return false
val hasMedia = selectedItem.media != null
val isPlaying = hasMedia && PlaybackStatus.isPlaying(selectedItem.media)
val isInQueue: Boolean = selectedItem.isTagged(FeedItem.TAG_QUEUE)

View File

@ -1,175 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Source: https://github.com/android/views-widgets-samples/blob/87e58d1/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt
* And modified for our need
*/
package ac.mdiq.podcini.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewTreeObserver
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import ac.mdiq.podcini.R
import kotlin.math.abs
/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
// KhaledAlharthi/NestedScrollableHost.java
class NestedScrollableHost : FrameLayout {
private var parentViewPager: ViewPager2? = null
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private var preferVertical = 1
private var preferHorizontal = 1
private var scrollDirection = 0
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context)
setAttributes(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context)
setAttributes(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?,
defStyleAttr: Int, defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context)
setAttributes(context, attrs)
}
private fun setAttributes(context: Context, attrs: AttributeSet?) {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.NestedScrollableHost, 0, 0)
try {
preferHorizontal = a.getInteger(R.styleable.NestedScrollableHost_preferHorizontal, 1)
preferVertical = a.getInteger(R.styleable.NestedScrollableHost_preferVertical, 1)
scrollDirection = a.getInteger(R.styleable.NestedScrollableHost_scrollDirection, 0)
} finally {
a.recycle()
}
}
private fun init(context: Context) {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
var v = parent as? View
while (v != null && v !is ViewPager2 || isntSameDirection(v)) {
v = v!!.parent as? View
}
parentViewPager = v as? ViewPager2
viewTreeObserver.removeOnPreDrawListener(this)
return false
}
})
}
private fun isntSameDirection(v: View?): Boolean {
val orientation: Int = when (scrollDirection) {
0 -> return false
1 -> ViewPager2.ORIENTATION_VERTICAL
2 -> ViewPager2.ORIENTATION_HORIZONTAL
else -> return false
}
return ((v is ViewPager2) && v.orientation != orientation)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
handleInterceptTouchEvent(ev)
return super.onInterceptTouchEvent(ev)
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.toInt()
val child = getChildAt(0)
return when (orientation) {
0 -> {
child.canScrollHorizontally(direction)
}
1 -> {
child.canScrollVertically(direction)
}
else -> {
throw IllegalArgumentException()
}
}
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
if (parentViewPager == null) {
return
}
val orientation = parentViewPager!!.orientation
val preferedDirection = preferHorizontal + preferVertical > 2
// Early return if child can't scroll in same direction as parent and theres no prefered scroll direction
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f) && !preferedDirection) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = (abs(dx.toDouble()) * (if (isVpHorizontal) 1f else 0.5f) * preferHorizontal).toFloat()
val scaledDy = (abs(dy.toDouble()) * (if (isVpHorizontal) 0.5f else 1f) * preferVertical).toFloat()
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(preferedDirection)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}

View File

@ -62,6 +62,7 @@ class ShownotesWebView : WebView, View.OnLongClickListener {
setOnLongClickListener(this)
setWebViewClient(object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (ShownotesCleaner.isTimecodeLink(url) && timecodeSelectedListener != null) {
timecodeSelectedListener!!.accept(ShownotesCleaner.getTimecodeLinkTime(url))
@ -173,6 +174,7 @@ class ShownotesWebView : WebView, View.OnLongClickListener {
this.pageFinishedListener = pageFinishedListener
}
@Deprecated("Deprecated in Java")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(max(measuredWidth, minimumWidth), max(measuredHeight, minimumHeight))

View File

@ -42,7 +42,7 @@ class WidgetUpdaterWorker(context: Context,
fun enqueueWork(context: Context) {
val workRequest: OneTimeWorkRequest = OneTimeWorkRequest.Builder(WidgetUpdaterWorker::class.java).build()
WorkManager.getInstance(context!!).enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, workRequest)
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, workRequest)
}
}
}

View File

@ -0,0 +1,8 @@
<vector android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color"
android:pathData="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
</vector>

View File

@ -31,15 +31,13 @@
app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="@drawable/ic_arrow_down" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/itemDescription"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_above="@id/playtime_layout"
android:layout_below="@id/toolbar"
android:layout_marginBottom="12dp"
android:foreground="?android:windowContentOverlay"
android:orientation="vertical" />
android:layout_marginBottom="12dp" />
<ImageView
android:layout_width="match_parent"
@ -52,7 +50,7 @@
android:id="@+id/cardViewSeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/pager"
android:layout_alignBottom="@+id/itemDescription"
android:layout_centerHorizontal="true"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
@ -104,7 +102,7 @@
android:layout_marginRight="8dp"
android:clickable="true"
android:max="500"
tools:progress="100" />
tools:progress="100"/>
<RelativeLayout
android:layout_width="match_parent"

View File

@ -1,172 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:squareImageView="http://schemas.android.com/apk/ac.mdiq.podcini"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cover_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/coverHolder"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:id="@+id/imgvCover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_marginHorizontal="32dp"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
squareImageView:direction="minimum"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:src="@android:drawable/sym_def_app_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/cover_fragment_text_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/txtvPodcastTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:ellipsize="none"
android:gravity="center_horizontal"
android:maxLines="2"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="false"
android:textSize="@dimen/text_size_small"
tools:text="Podcast" />
<TextView
android:id="@+id/txtvEpisodeTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="none"
android:gravity="center_horizontal"
android:maxLines="2"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorPrimary"
android:textIsSelectable="false"
android:textSize="@dimen/text_size_small"
android:layout_marginBottom="32dp"
tools:text="Episode" />
</LinearLayout>
<LinearLayout
android:id="@+id/episode_details"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:layout_gravity="center_horizontal"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<LinearLayout
android:id="@+id/openDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:paddingHorizontal="8dp"
android:background="@drawable/grey_border"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:minWidth="150dp"
android:layout_weight="1"
android:orientation="horizontal">
<ImageView
android:id="@+id/description_icon"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:contentDescription="@string/shownotes_contentdescription"
android:padding="2dp"
app:srcCompat="@drawable/ic_info" />
<TextView
android:id="@+id/shownotes_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="none"
android:layout_marginLeft="2dp"
android:layout_marginStart="2dp"
android:gravity="center_horizontal"
android:maxLines="2"
android:text="@string/shownotes_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/chapterButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_weight="1"
android:background="@drawable/grey_border"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:minWidth="150dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<ImageButton
android:id="@+id/butPrevChapter"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/prev_chapter"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_chapter_prev" />
<TextView
android:id="@+id/chapters_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/chapters_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_navdrawer" />
<ImageButton
android:id="@+id/butNextChapter"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/next_chapter"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_chapter_next" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,135 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/echoImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView
android:id="@+id/echoProgressImage"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:importantForAccessibility="no"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
<ImageView
android:id="@+id/closeButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="16dp"
android:src="@drawable/ic_close_white"
android:contentDescription="@string/close_label"
android:layout_alignParentEnd="true"
android:layout_below="@id/echoProgressImage" />
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="16dp"
android:src="@drawable/logo_monochrome"
android:importantForAccessibility="no"
android:layout_alignParentStart="true"
android:layout_below="@id/echoProgressImage" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="32dp"
android:orientation="vertical">
<TextView
android:id="@+id/aboveLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:fontFamily="@font/sarabun_regular"
app:fontFamily="@font/sarabun_regular"
tools:text="text above"
style="@style/TextAppearance.Material3.TitleLarge" />
<TextView
android:id="@+id/largeLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:layout_marginVertical="8dp"
android:fontFamily="@font/sarabun_semi_bold"
app:fontFamily="@font/sarabun_semi_bold"
tools:text="large"
style="@style/TextAppearance.Material3.DisplayLarge"
tools:targetApi="p" />
<TextView
android:id="@+id/belowLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:fontFamily="@font/sarabun_regular"
app:fontFamily="@font/sarabun_regular"
tools:text="text below"
style="@style/TextAppearance.Material3.TitleLarge" />
<TextView
android:id="@+id/smallLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="#ffffff"
android:textSize="16sp"
android:layout_marginTop="32dp"
android:fontFamily="@font/sarabun_regular"
app:fontFamily="@font/sarabun_regular"
tools:text="small" />
</LinearLayout>
<ImageView
android:id="@+id/echoLogo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_margin="32dp"
android:src="@drawable/echo"
android:importantForAccessibility="no"
android:layout_alignParentBottom="true" />
<com.google.android.material.button.MaterialButton
android:id="@+id/shareButton"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_margin="32dp"
android:text="@string/share_label"
android:drawableLeft="@drawable/ic_share"
android:textColor="#fff"
android:contentDescription="@string/share_label"
style="@style/Widget.Material3.Button.OutlinedButton"
app:strokeColor="#fff"
tools:ignore="RtlHardcoded" />
</RelativeLayout>
</RelativeLayout>

View File

@ -207,19 +207,12 @@
</LinearLayout>
<ac.mdiq.podcini.ui.view.NestedScrollableHost
<ac.mdiq.podcini.ui.view.ShownotesWebView
android:id="@+id/webvDescription"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/header"
app:preferVertical="3">
<ac.mdiq.podcini.ui.view.ShownotesWebView
android:id="@+id/webvDescription"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:windowContentOverlay" />
</ac.mdiq.podcini.ui.view.NestedScrollableHost>
android:foreground="?android:windowContentOverlay" />
<FrameLayout
android:layout_width="match_parent"

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ac.mdiq.podcini.ui.view.NestedScrollableHost
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/item_description_fragment"
android:fillViewport="false"
app:preferVertical="10"
android:nestedScrollingEnabled="true">
<ac.mdiq.podcini.ui.view.ShownotesWebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</ac.mdiq.podcini.ui.view.NestedScrollableHost>

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/item_description_fragment"
android:fillViewport="false">
<LinearLayout
android:id="@+id/playtime_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtvPodcastTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="2"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:textSize="@dimen/text_size_large"
android:layout_marginBottom="5dp"
tools:text="Podcast" />
<TextView
android:id="@+id/episodeDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:maxLines="1"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorPrimary"
android:textIsSelectable="false"
android:textSize="14sp"
tools:text="Episode" />
<TextView
android:id="@+id/txtvEpisodeTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="2"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:textSize="18sp"
tools:text="Episode" />
<ac.mdiq.podcini.ui.view.ShownotesWebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:id="@+id/chapterButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_margin="20dp"
android:background="@drawable/grey_border"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:minWidth="150dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<ImageButton
android:id="@+id/butPrevChapter"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/prev_chapter"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_chapter_prev" />
<TextView
android:id="@+id/chapters_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/chapters_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_navdrawer" />
<ImageButton
android:id="@+id/butNextChapter"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/next_chapter"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_chapter_next" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/coverHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<ImageView
android:id="@+id/imgvCover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_marginHorizontal="32dp"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:src="@android:drawable/sym_def_app_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>

View File

@ -14,13 +14,28 @@
android:background="#262C31"
tools:ignore="UselessParent">
<TextView
android:id="@+id/txtvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="12dp"
android:gravity="center"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="gone" />
<ImageButton
android:id="@+id/butPlay"
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="match_parent"
android:contentDescription="@string/play_label"
android:layout_alignParentEnd="true"
android:layout_margin="12dp"
android:layout_below="@id/txtvTitle"
android:layout_marginHorizontal="12dp"
android:background="?android:attr/selectableItemBackground"
android:scaleType="fitCenter"
android:padding="8dp"
@ -31,6 +46,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_below="@id/txtvTitle"
android:layout_toStartOf="@id/butPlay"
android:background="@android:color/transparent"
android:gravity="fill_horizontal"
@ -42,7 +58,8 @@
android:layout_height="match_parent"
android:src="@mipmap/ic_launcher"
android:importantForAccessibility="no"
android:layout_margin="12dp" />
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"/>
<LinearLayout
android:id="@+id/layout_center"
@ -61,17 +78,6 @@
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
<TextView
android:id="@+id/txtvProgress"
android:layout_width="wrap_content"

View File

@ -32,12 +32,14 @@
<item
android:id="@+id/add_to_favorites_item"
android:menuCategory="container"
android:icon="@drawable/ic_star_border"
custom:showAsAction="ifRoom|collapseActionView"
android:title="@string/add_to_favorite_label" />
<item
android:id="@+id/remove_from_favorites_item"
android:menuCategory="container"
android:icon="@drawable/ic_star"
custom:showAsAction="ifRoom|collapseActionView"
android:title="@string/remove_from_favorite_label" />
<item
@ -53,12 +55,11 @@
</item>
<item
android:id="@+id/share_item"
android:menuCategory="container"
android:icon="@drawable/ic_share"
android:title="@string/share_label">
</item>
<item
android:id="@+id/share_notes"
android:menuCategory="container"
android:title="@string/share_notes_label">
</item>
<item

View File

@ -47,6 +47,7 @@
<item
android:id="@+id/share_item"
android:icon="@drawable/ic_share"
android:menuCategory="container"
android:title="@string/share_label" />

View File

@ -28,6 +28,14 @@
android:title="@string/set_sleeptimer_label">
</item>
<item
android:id="@+id/player_switch_to_audio_only"
custom:showAsAction="always"
android:icon="@drawable/baseline_audiotrack_24"
android:title="@string/player_switch_to_audio_only"
android:visible="false">
</item>
<item
android:id="@+id/audio_controls"
android:title="@string/audio_controls"
@ -58,13 +66,6 @@
android:visible="false">
</item>
<item
android:id="@+id/player_switch_to_audio_only"
custom:showAsAction="collapseActionView"
android:title="@string/player_switch_to_audio_only"
android:visible="false">
</item>
<item
android:id="@+id/player_show_chapters"
custom:showAsAction="never"
@ -76,7 +77,12 @@
android:id="@+id/share_item"
android:icon="@drawable/ic_share"
android:menuCategory="container"
custom:showAsAction="always"
custom:showAsAction="ifRoom"
android:title="@string/share_label">
</item>
<item
android:id="@+id/share_notes"
android:title="@string/share_notes_label">
</item>
</menu>

View File

@ -777,7 +777,7 @@
<!-- Audio controls -->
<string name="audio_controls">Audio controls</string>
<string name="playback_speed">Playback speed</string>
<string name="player_switch_to_audio_only">Switch to audio only</string>
<string name="player_switch_to_audio_only">Audio only</string>
<!-- proxy settings -->
<string name="proxy_type_label">Type</string>

View File

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:resizeMode="horizontal"
android:initialLayout="@layout/player_widget"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/ic_widget_preview"
android:minHeight="40dp"
android:minWidth="250dp"
android:minResizeWidth="40dp"
android:widgetFeatures="reconfigurable"
android:configure="ac.mdiq.podcini.ui.activity.WidgetConfigActivity">
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:resizeMode="horizontal|vertical"
android:initialLayout="@layout/player_widget"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/ic_widget_preview"
android:minHeight="40dp"
android:minWidth="100dp"
android:minResizeWidth="70dp"
android:configure="ac.mdiq.podcini.ui.activity.WidgetConfigActivity"
android:widgetFeatures="reconfigurable">
</appwidget-provider>

View File

@ -124,4 +124,16 @@
* further optimized efficiencies of episode info view
* episode info view opened from icon is now the same as that opened from title area, no long supports horizontal swipes (change from 4.2.7)
* enhanced viewbingding GC
* some code cleaning
* some code cleaning
## 4.3.3
* fixed bug in adding widget to home screen
* minor adjustment of widget layout
* added "audio only" to action bar in video player
* added "mark favorite" to action bar in episode view
* revamped and enhanced expanded view of the player
* vertical swipe no longer collapses the expanded view
* only the down arrow on top left page collapses the expanded view
* share notes directly from expanded view of the player
* in episode info, changed rendering of description, removed nested scroll

View File

@ -0,0 +1,12 @@
Version 4.3.3 brings several changes:
* fixed bug in adding widget to home screen
* minor adjustment of widget layout
* added "audio only" to action bar in video player
* added "mark favorite" to action bar in episode view
* revamped and enhanced expanded view of the player
* vertical swipe no longer collapses the expanded view
* only the down arrow on top left page collapses the expanded view
* share notes directly from expanded view of the player
* in episode info, changed rendering of description, removed nested scroll