From 5cb3b20b00c81a272f91297fefee4e67d547bb90 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Sun, 24 Mar 2024 20:36:12 +0000 Subject: [PATCH] 4.3.3 release --- README.md | 4 +- app/build.gradle | 4 +- .../podcini/ui/activity/BugReportActivity.kt | 2 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 22 +- .../ui/activity/VideoplayerActivity.kt | 7 - .../ui/activity/WidgetConfigActivity.kt | 14 +- .../podcini/ui/dialog/ItemFilterDialog.kt | 13 +- .../ui/fragment/AudioPlayerFragment.kt | 160 +++++----- .../ui/fragment/EpisodeInfoFragment.kt | 29 +- .../ui/fragment/ItemDescriptionFragment.kt | 173 ---------- ...erFragment.kt => PlayerDetailsFragment.kt} | 295 ++++++++++++------ .../ui/menuhandler/FeedItemMenuHandler.kt | 5 +- .../podcini/ui/view/NestedScrollableHost.kt | 175 ----------- .../mdiq/podcini/ui/view/ShownotesWebView.kt | 2 + .../podcini/ui/widget/WidgetUpdaterWorker.kt | 2 +- .../res/drawable/baseline_audiotrack_24.xml | 8 + .../main/res/layout/audioplayer_fragment.xml | 12 +- app/src/main/res/layout/cover_fragment.xml | 172 ---------- app/src/main/res/layout/echo_activity.xml | 135 -------- .../main/res/layout/episode_info_fragment.xml | 13 +- .../res/layout/item_description_fragment.xml | 17 - .../res/layout/player_details_fragment.xml | 135 ++++++++ app/src/main/res/layout/player_widget.xml | 32 +- app/src/main/res/menu/feeditem_options.xml | 9 +- .../main/res/menu/feeditemlist_context.xml | 1 + app/src/main/res/menu/mediaplayer.xml | 22 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/player_widget_info.xml | 21 +- changelog.md | 14 +- .../android/en-US/changelogs/3020114.txt | 12 + 30 files changed, 543 insertions(+), 969 deletions(-) delete mode 100644 app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemDescriptionFragment.kt rename app/src/main/java/ac/mdiq/podcini/ui/fragment/{CoverFragment.kt => PlayerDetailsFragment.kt} (62%) delete mode 100644 app/src/main/java/ac/mdiq/podcini/ui/view/NestedScrollableHost.kt create mode 100644 app/src/main/res/drawable/baseline_audiotrack_24.xml delete mode 100644 app/src/main/res/layout/cover_fragment.xml delete mode 100644 app/src/main/res/layout/echo_activity.xml delete mode 100644 app/src/main/res/layout/item_description_fragment.xml create mode 100644 app/src/main/res/layout/player_details_fragment.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/3020114.txt diff --git a/README.md b/README.md index a595ba3d..fe0279f2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Podcini is an open source podcast manager/player project. -This app is a fork of [AntennaPod]() as of Feb 5 2024. +This project is a fork of [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. +5. Aims to improve efficiency and provide more user-friendly features ## Version 4 diff --git a/app/build.gradle b/app/build.gradle index e2e0133d..2b1c21a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/BugReportActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/BugReportActivity.kt index 6f618acc..1405411b 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/BugReportActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/BugReportActivity.kt @@ -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 { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt index de2ea2a1..6e9ca9fa 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -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) } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index 3f5c5c26..5e4d1c1a 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -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)) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt index 84bb92f6..8824eda7 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt @@ -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) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemFilterDialog.kt index 1cca0ab1..2ebd2243 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemFilterDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemFilterDialog.kt @@ -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 diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 9dfe5f2f..5d965994 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -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 } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 9421e5d2..34929168 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -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!!) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemDescriptionFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemDescriptionFragment.kt deleted file mode 100644 index 7bb978ad..00000000 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemDescriptionFragment.kt +++ /dev/null @@ -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 -> - 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" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/CoverFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt similarity index 62% rename from app/src/main/java/ac/mdiq/podcini/ui/fragment/CoverFragment.kt rename to app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index 745ff39f..109ecfb0 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/CoverFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -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 -> + 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 { emitter: MaybeEmitter -> - 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 = 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 = 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" } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/menuhandler/FeedItemMenuHandler.kt b/app/src/main/java/ac/mdiq/podcini/ui/menuhandler/FeedItemMenuHandler.kt index 2ba47bf3..10322903 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/menuhandler/FeedItemMenuHandler.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/menuhandler/FeedItemMenuHandler.kt @@ -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) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/view/NestedScrollableHost.kt b/app/src/main/java/ac/mdiq/podcini/ui/view/NestedScrollableHost.kt deleted file mode 100644 index 682b2fd9..00000000 --- a/app/src/main/java/ac/mdiq/podcini/ui/view/NestedScrollableHost.kt +++ /dev/null @@ -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) - } - } - } - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/ui/view/ShownotesWebView.kt b/app/src/main/java/ac/mdiq/podcini/ui/view/ShownotesWebView.kt index ed1ba853..21af30b3 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/view/ShownotesWebView.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/view/ShownotesWebView.kt @@ -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)) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt index 68466eec..1212d007 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt @@ -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) } } } diff --git a/app/src/main/res/drawable/baseline_audiotrack_24.xml b/app/src/main/res/drawable/baseline_audiotrack_24.xml new file mode 100644 index 00000000..2c248ea7 --- /dev/null +++ b/app/src/main/res/drawable/baseline_audiotrack_24.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml index dd5c1cd0..e5113d5a 100644 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -31,15 +31,13 @@ app:navigationContentDescription="@string/toolbar_back_button_content_description" app:navigationIcon="@drawable/ic_arrow_down" /> - + android:layout_marginBottom="12dp" /> + tools:progress="100"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/echo_activity.xml b/app/src/main/res/layout/echo_activity.xml deleted file mode 100644 index 3c5590d3..00000000 --- a/app/src/main/res/layout/echo_activity.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/episode_info_fragment.xml b/app/src/main/res/layout/episode_info_fragment.xml index 6c765060..96b73097 100644 --- a/app/src/main/res/layout/episode_info_fragment.xml +++ b/app/src/main/res/layout/episode_info_fragment.xml @@ -207,19 +207,12 @@ - - - - - + android:foreground="?android:windowContentOverlay" /> - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/player_details_fragment.xml b/app/src/main/res/layout/player_details_fragment.xml new file mode 100644 index 00000000..ed95b116 --- /dev/null +++ b/app/src/main/res/layout/player_details_fragment.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_widget.xml b/app/src/main/res/layout/player_widget.xml index fde313b5..30adbc0c 100644 --- a/app/src/main/res/layout/player_widget.xml +++ b/app/src/main/res/layout/player_widget.xml @@ -14,13 +14,28 @@ android:background="#262C31" tools:ignore="UselessParent"> + + + android:layout_marginStart="12dp" + android:layout_marginEnd="12dp"/> - - diff --git a/app/src/main/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml index c7cae491..4534dd98 100644 --- a/app/src/main/res/menu/mediaplayer.xml +++ b/app/src/main/res/menu/mediaplayer.xml @@ -28,6 +28,14 @@ android:title="@string/set_sleeptimer_label"> + + + - - - + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fda2d144..2e661e25 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -777,7 +777,7 @@ Audio controls Playback speed - Switch to audio only + Audio only Type diff --git a/app/src/main/res/xml/player_widget_info.xml b/app/src/main/res/xml/player_widget_info.xml index 81279397..106e9c8b 100644 --- a/app/src/main/res/xml/player_widget_info.xml +++ b/app/src/main/res/xml/player_widget_info.xml @@ -1,12 +1,13 @@ - + diff --git a/changelog.md b/changelog.md index 0911adf6..09a7fccb 100644 --- a/changelog.md +++ b/changelog.md @@ -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 \ No newline at end of file +* 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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/3020114.txt b/fastlane/metadata/android/en-US/changelogs/3020114.txt new file mode 100644 index 00000000..8c566149 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020114.txt @@ -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