From 97a402cd47d6a137ff4f77a0bd8b634d52a45275 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:52:20 +0100 Subject: [PATCH] 6.1.5 commit --- README.md | 13 ++-- app/build.gradle | 12 ++-- .../playback/service/LocalMediaPlayer.kt | 2 +- .../mdiq/podcini/storage/database/Episodes.kt | 21 ++++-- .../mdiq/podcini/storage/database/Queues.kt | 28 +++++--- .../mdiq/podcini/storage/database/RealmDB.kt | 2 +- .../mdiq/podcini/storage/model/PlayQueue.kt | 7 ++ .../ui/actions/EpisodeMultiSelectHandler.kt | 27 +++++--- .../ui/fragment/BaseEpisodesFragment.kt | 15 +++-- .../podcini/ui/fragment/DownloadsFragment.kt | 5 +- .../ui/fragment/FeedEpisodesFragment.kt | 4 +- .../podcini/ui/fragment/FeedInfoFragment.kt | 62 +++++++---------- .../mdiq/podcini/ui/fragment/QueueFragment.kt | 62 +++++++++++------ .../res/drawable/trash_can_arrow_up_solid.xml | 7 ++ app/src/main/res/layout/feedinfo.xml | 63 +++++++++++------- app/src/main/res/layout/feedinfo_header.xml | 2 +- .../menu/episodes_apply_action_speeddial.xml | 36 ++++++---- app/src/main/res/menu/queue.xml | 16 ++++- app/src/main/res/values/strings.xml | 5 ++ changelog.md | 10 +++ .../android/en-US/changelogs/3020219.txt | 11 +++ images/3_subscriptions.jpg | Bin 295984 -> 281758 bytes images/4_queue1.jpg | Bin 192471 -> 198623 bytes images/5_podcast_0.jpg | Bin 269812 -> 275108 bytes images/5_podcast_1.jpg | Bin 265916 -> 271845 bytes images/5_podcast_setting1.jpg | Bin 211891 -> 220267 bytes images/6_episode.jpg | Bin 325427 -> 335641 bytes 27 files changed, 257 insertions(+), 153 deletions(-) create mode 100644 app/src/main/res/drawable/trash_can_arrow_up_solid.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/3020219.txt diff --git a/README.md b/README.md index 4fdb652a..e39572df 100644 --- a/README.md +++ b/README.md @@ -79,14 +79,14 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view) * in all episodes list views, click on an episode image brings up the FeedInfo view * in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward) -* Long-press filter button in FeedEpisode view enables/disables filters without changing filter settings +* on action bar of FeedEpisodes view there is a direct access to Queue +* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings * History view shows time of last play, and allows filters and sorts * Multiple queues can be used: 5 queues are provided by default: Default queue, and Queues 1-4 * all queue operations are on the curQueue, which can be set in all episodes list views * on app startup, the most recently updated queue is set to curQueue -* queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played -* on action bar of FeedEpisodes view there is a direct access to Queue - +* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played +* Every queue has a bin of past episodes added to the queue ### Podcast/Episode @@ -133,8 +133,9 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure * Settings/Preferences can now be exported and imported * Play history/progress can be separately exported/imported as Json files -* downloaded media files can be exported/imported -* There is a setting to disable/enable auto backup OPML files to Google +* Downloaded media files can be exported/imported +* There is a setting to disable/enable auto backup of OPML files to Google +* Upon re-install of Podcini, the OPML file previously backed up to Google is not imported automatically but based on user confirmation. For more details of the changes, see the [Changelog](changelog.md) diff --git a/app/build.gradle b/app/build.gradle index 73ac2283..c4feeaf9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -126,8 +126,8 @@ android { buildConfig true } defaultConfig { - versionCode 3020218 - versionName "6.1.4" + versionCode 3020219 + versionName "6.1.5" applicationId "ac.mdiq.podcini.R" def commit = "" @@ -214,13 +214,13 @@ dependencies { implementation "com.google.android.material:material:1.12.0" - implementation 'io.realm.kotlin:library-base:2.0.0' + implementation 'io.realm.kotlin:library-base:2.1.0' - implementation "org.apache.commons:commons-lang3:3.14.0" + implementation 'org.apache.commons:commons-lang3:3.15.0' implementation 'commons-io:commons-io:2.16.1' - implementation "org.jsoup:jsoup:1.17.2" + implementation 'org.jsoup:jsoup:1.18.1' - implementation "io.coil-kt:coil:2.6.0" + implementation 'io.coil-kt:coil:2.7.0' implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index d2ad22cc..664d095a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -550,7 +550,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP callback.onPlaybackEnded(nextMedia.getMediaType(), false) // setting media to null signals to playMediaObject that we're taking care of post-playback processing curMedia = null - playMediaObject(nextMedia, !nextMedia.localFileAvailable(), isPlaying, isPlaying) + if(nextMedia != null) playMediaObject(nextMedia, !nextMedia.localFileAvailable(), isPlaying, isPlaying) } } when { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 16df02e8..11e8c11e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -10,7 +10,6 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE -import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.Prefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesSync @@ -20,14 +19,18 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.Episode -import ac.mdiq.podcini.storage.model.EpisodeMedia +import ac.mdiq.podcini.storage.model.Episode.Companion.BUILDING +import ac.mdiq.podcini.storage.model.Episode.Companion.NEW +import ac.mdiq.podcini.storage.model.Episode.Companion.PLAYED +import ac.mdiq.podcini.storage.model.Episode.Companion.UNPLAYED import ac.mdiq.podcini.storage.model.EpisodeFilter +import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeSortOrder +import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import android.app.backup.BackupManager import android.content.Context import android.net.Uri @@ -256,11 +259,11 @@ object Episodes { } @JvmStatic - fun setFavorite(episode: Episode, stat: Boolean) : Job { + fun setFavorite(episode: Episode, stat: Boolean?) : Job { Logd(TAG, "setFavorite called $stat") return runOnIOScope { val result = upsert(episode) { - it.isFavorite = stat + it.isFavorite = stat ?: !it.isFavorite } EventFlow.postEvent(FlowEvent.FavoritesEvent(result)) } @@ -286,10 +289,14 @@ object Episodes { suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode { Logd(TAG, "setPlayStateSync called resetMediaPosition: $resetMediaPosition") val result = upsert(episode) { - it.playState = played + if (played >= NEW && played <= BUILDING) it.playState = played + else { + if (it.playState == UNPLAYED) it.playState = PLAYED + else if (it.playState == PLAYED) it.playState = UNPLAYED + } if (resetMediaPosition) it.media?.setPosition(0) } - if (played == Episode.PLAYED && shouldRemoveFromQueuesMarkPlayed()) removeFromAllQueuesSync(result) + if (played == PLAYED && shouldRemoveFromQueuesMarkPlayed()) removeFromAllQueuesSync(result) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result)) return result } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index 5dc0a08c..e36f15f4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -193,6 +193,7 @@ object Queues { return runOnIOScope { curQueue.update() curQueue.episodes.clear() + curQueue.idsBin.addAll(curQueue.episodeIds) curQueue.episodeIds.clear() upsert(curQueue) {} EventFlow.postEvent(FlowEvent.QueueEvent.cleared()) @@ -229,18 +230,21 @@ object Queues { var queue = queue_ ?: curQueue val events: MutableList = ArrayList() - val pos: MutableList = mutableListOf() + val indicesToRemove: MutableList = mutableListOf() val qItems = queue.episodes.toMutableList() for (i in qItems.indices) { val episode = qItems[i] if (episodes.contains(episode)) { Logd(TAG, "removing from queue: ${episode.id} ${episode.title}") - pos.add(i) + indicesToRemove.add(i) if (queue.id == curQueue.id) events.add(FlowEvent.QueueEvent.removed(episode)) } } - if (pos.isNotEmpty()) { - for (i in pos.indices.reversed()) qItems.removeAt(pos[i]) + if (indicesToRemove.isNotEmpty()) { + for (i in indicesToRemove.indices.reversed()) { + queue.idsBin.add(qItems[indicesToRemove[i]].id) + qItems.removeAt(indicesToRemove[i]) + } queue.update() queue.episodeIds.clear() for (e in qItems) queue.episodeIds.add(e.id) @@ -255,13 +259,14 @@ object Queues { suspend fun removeFromAllQueuesQuiet(episodeIds: List) { Logd(TAG, "removeFromAllQueuesQuiet called ") - var eidsInQueues: MutableSet + var idsInQueuesToRemove: MutableSet val queues = realm.query(PlayQueue::class).find() for (q in queues) { if (q.id == curQueue.id) continue - eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() - if (eidsInQueues.isNotEmpty()) { - val qeids = q.episodeIds.minus(eidsInQueues) + idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() + if (idsInQueuesToRemove.isNotEmpty()) { + q.idsBin.addAll(idsInQueuesToRemove) + val qeids = q.episodeIds.minus(idsInQueuesToRemove) upsert(q) { it.episodeIds.clear() it.episodeIds.addAll(qeids) @@ -271,9 +276,10 @@ object Queues { } // ensure curQueue is last updated val q = curQueue - eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() - if (eidsInQueues.isNotEmpty()) { - val qeids = q.episodeIds.minus(eidsInQueues) + idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() + if (idsInQueuesToRemove.isNotEmpty()) { + q.idsBin.addAll(idsInQueuesToRemove) + val qeids = q.episodeIds.minus(idsInQueuesToRemove) upsert(q) { it.episodeIds.clear() it.episodeIds.addAll(qeids) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index b4faf82b..977373c1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -18,7 +18,7 @@ import kotlin.coroutines.ContinuationInterceptor object RealmDB { private val TAG: String = RealmDB::class.simpleName ?: "Anonymous" - private const val SCHEMA_VERSION_NUMBER = 11L + private const val SCHEMA_VERSION_NUMBER = 13L private val ioScope = CoroutineScope(Dispatchers.IO) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt index aa6d878a..5c6b8123 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt @@ -1,8 +1,10 @@ package ac.mdiq.podcini.storage.model import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.annotations.Ignore import io.realm.kotlin.types.annotations.PrimaryKey import java.util.* @@ -22,6 +24,11 @@ class PlayQueue : RealmObject { @Ignore val episodes: MutableList = mutableListOf() + var idsBin: RealmSet = realmSetOf() + +// @Ignore +// val episodesBin: MutableList = mutableListOf() + fun isInQueue(episode: Episode): Boolean { return episodeIds.contains(episode.id) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt index 6985ca7d..dedabefe 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt @@ -39,19 +39,24 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val fun handleAction(items: List) { when (actionId) { - R.id.add_to_favorite_batch -> markFavorite(items, true) - R.id.remove_favorite_batch -> markFavorite(items, false) + R.id.toggle_favorite_batch -> toggleFavorite(items) +// R.id.add_to_favorite_batch -> markFavorite(items, true) +// R.id.remove_favorite_batch -> markFavorite(items, false) R.id.add_to_queue_batch -> queueChecked(items) R.id.put_in_queue_batch -> PutToQueueDialog(activity, items).show() R.id.remove_from_queue_batch -> removeFromQueueChecked(items) - R.id.mark_read_batch -> { - setPlayState(Episode.PLAYED, false, *items.toTypedArray()) - showMessage(R.plurals.marked_read_batch_label, items.size) - } - R.id.mark_unread_batch -> { - setPlayState(Episode.UNPLAYED, false, *items.toTypedArray()) - showMessage(R.plurals.marked_unread_batch_label, items.size) + R.id.toggle_played_batch -> { + setPlayState(-2, false, *items.toTypedArray()) +// showMessage(R.plurals.marked_read_batch_label, items.size) } +// R.id.mark_read_batch -> { +// setPlayState(Episode.PLAYED, false, *items.toTypedArray()) +// showMessage(R.plurals.marked_read_batch_label, items.size) +// } +// R.id.mark_unread_batch -> { +// setPlayState(Episode.UNPLAYED, false, *items.toTypedArray()) +// showMessage(R.plurals.marked_unread_batch_label, items.size) +// } R.id.download_batch -> downloadChecked(items) R.id.delete_batch -> deleteChecked(items) else -> Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=$actionId") @@ -74,9 +79,9 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.size) } - private fun markFavorite(items: List, stat: Boolean) { + private fun toggleFavorite(items: List) { for (item in items) { - Episodes.setFavorite(item, true) + Episodes.setFavorite(item, null) } showMessage(R.plurals.marked_favorite_batch_label, items.size) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt index fba3eacd..17c3b694 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt @@ -110,7 +110,7 @@ import kotlinx.coroutines.flow.collectLatest swipeRefreshLayout = binding.swipeRefresh swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance)) - swipeRefreshLayout.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext()) } +// swipeRefreshLayout.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext()) } createListAdaptor() @@ -145,8 +145,9 @@ import kotlinx.coroutines.flow.collectLatest if (adapter.selectedItems.size >= 25 || adapter.shouldSelectLazyLoadedItems()) { // Should ask for confirmation when (actionItem.id) { - R.id.mark_read_batch -> confirmationString = R.string.multi_select_mark_played_confirmation - R.id.mark_unread_batch -> confirmationString = R.string.multi_select_mark_unplayed_confirmation +// R.id.mark_read_batch -> confirmationString = R.string.multi_select_mark_played_confirmation +// R.id.mark_unread_batch -> confirmationString = R.string.multi_select_mark_unplayed_confirmation + R.id.toggle_played_batch -> confirmationString = R.string.multi_select_toggle_played_confirmation } } if (confirmationString == 0) performMultiSelectAction(actionItem.id) @@ -206,10 +207,10 @@ import kotlinx.coroutines.flow.collectLatest val itemId = item.itemId when (itemId) { - R.id.refresh_item -> { - FeedUpdateManager.runOnceOrAsk(requireContext()) - return true - } +// R.id.refresh_item -> { +// FeedUpdateManager.runOnceOrAsk(requireContext()) +// return true +// } R.id.action_search -> { (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) return true diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index deb67e63..e0918b4b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -4,7 +4,6 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding import ac.mdiq.podcini.databinding.SimpleListFragmentBinding import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface -import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs @@ -120,8 +119,6 @@ import java.util.* speedDialView.overlayLayout = multiSelectDial.fabSDOverlay speedDialView.inflate(R.menu.episodes_apply_action_speeddial) speedDialView.removeActionItemById(R.id.download_batch) -// speedDialView.removeActionItemById(R.id.mark_read_batch) -// speedDialView.removeActionItemById(R.id.mark_unread_batch) speedDialView.removeActionItemById(R.id.remove_from_queue_batch) speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener { override fun onMainActionSelected(): Boolean { @@ -178,7 +175,7 @@ import java.util.* @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { - R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) +// R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) R.id.action_download_logs -> DownloadLogFragment().show(childFragmentManager, null) R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) R.id.downloads_sort -> DownloadsSortDialog().show(childFragmentManager, "SortDialog") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index 8f2dd585..52b00846 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -70,8 +70,8 @@ import java.util.concurrent.Semaphore /** * Displays a list of FeedItems. */ -@UnstableApi class FeedEpisodesFragment : Fragment(), - AdapterView.OnItemClickListener, Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener { +@UnstableApi class FeedEpisodesFragment + : Fragment(), AdapterView.OnItemClickListener, Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener { private var _binding: FeedItemListFragmentBinding? = null private val binding get() = _binding!! diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index 77bed85d..8f8dda29 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -11,7 +11,6 @@ import ac.mdiq.podcini.storage.database.Feeds.updateFeedDownloadURL import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.FeedFunding import ac.mdiq.podcini.ui.activity.MainActivity -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.statistics.FeedStatisticsFragment import ac.mdiq.podcini.ui.statistics.StatisticsFragment import ac.mdiq.podcini.ui.utils.ToolbarIconTintManager @@ -23,6 +22,7 @@ import android.R.string import android.app.Activity import android.content.* import android.content.res.Configuration +import android.graphics.LightingColorFilter import android.net.Uri import android.os.Build import android.os.Bundle @@ -32,7 +32,6 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.ImageView import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources @@ -65,8 +64,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { private val binding get() = _binding!! private lateinit var feed: Feed - private lateinit var imgvCover: ImageView - private lateinit var imgvBackground: ImageView +// private lateinit var imgvCover: ImageView +// private lateinit var imgvBackground: ImageView private lateinit var toolbar: MaterialToolbar private val addLocalFolderLauncher = registerForActivityResult(AddLocalFolder()) { @@ -96,48 +95,38 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { val appBar: AppBarLayout = binding.appBar val collapsingToolbar: CollapsingToolbarLayout = binding.collapsingToolbar - val iconTintManager: ToolbarIconTintManager = - object : ToolbarIconTintManager(requireContext(), toolbar, collapsingToolbar) { - override fun doTint(themedContext: Context) { - toolbar.menu.findItem(R.id.visit_website_item).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_web)) - toolbar.menu.findItem(R.id.share_item).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_share)) - } + val iconTintManager: ToolbarIconTintManager = object : ToolbarIconTintManager(requireContext(), toolbar, collapsingToolbar) { + override fun doTint(themedContext: Context) { + toolbar.menu.findItem(R.id.visit_website_item).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_web)) + toolbar.menu.findItem(R.id.share_item).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_share)) } + } iconTintManager.updateTint() appBar.addOnOffsetChangedListener(iconTintManager) - imgvCover = binding.header.imgvCover - imgvBackground = binding.imgvBackground +// imgvCover = binding.header.imgvCover +// imgvBackground = binding.imgvBackground // https://github.com/bumptech/glide/issues/529 -// imgvBackground.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000) + binding.imgvBackground.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000) binding.header.episodes.text = feed.episodes.size.toString() + " episodes" binding.header.episodes.setOnClickListener { - val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id) - (activity as MainActivity).loadChildFragment(fragment) + (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id)) } binding.header.butShowSettings.setOnClickListener { - val fragment = FeedSettingsFragment.newInstance(feed) - (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE) + (activity as MainActivity).loadChildFragment(FeedSettingsFragment.newInstance(feed), TransitionEffect.SLIDE) } binding.btnvRelatedFeeds.setOnClickListener { val fragment = OnlineSearchFragment.newInstance(CombinedSearcher::class.java, "${binding.header.txtvAuthor.text} podcasts") (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE) } - binding.txtvUrl.setOnClickListener(copyUrlToClipboard) - - val feedId = feed.id parentFragmentManager.beginTransaction().replace(R.id.statisticsFragmentContainer, - FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment") - .commitAllowingStateLoss() - + FeedStatisticsFragment.newInstance(feed.id, false), "feed_statistics_fragment").commitAllowingStateLoss() binding.btnvOpenStatistics.setOnClickListener { - val fragment = StatisticsFragment() - (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE) + (activity as MainActivity).loadChildFragment(StatisticsFragment(), TransitionEffect.SLIDE) } - showFeed() return binding.root } @@ -150,21 +139,21 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } private fun showFeed() { - Logd(TAG, "Language is " + feed.language) - Logd(TAG, "Author is " + feed.author) - Logd(TAG, "URL is " + feed.downloadUrl) + Logd(TAG, "Language: ${feed.language} Author: ${feed.author}") + Logd(TAG, "URL: ${feed.downloadUrl}") // TODO: need to generate blurred image for background - imgvCover.load(feed.imageUrl) { + binding.header.imgvCover.load(feed.imageUrl) { placeholder(R.color.light_gray) error(R.mipmap.ic_launcher) } - binding.header.txtvTitle.text = feed.title - binding.header.txtvTitle.setMaxLines(6) + binding.header.txtvTitle.setMaxLines(3) + + binding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"") - val description: String = HtmlToPlainText.getPlainText(feed.description?:"") - binding.txtvDescription.text = description + binding.feedTitle.text = feed.title + binding.feedAuthor.text = feed.author if (!feed.author.isNullOrEmpty()) binding.header.txtvAuthor.text = feed.author @@ -224,10 +213,6 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } override fun onMenuItemClick(item: MenuItem): Boolean { -// if (feed == null) { -// (activity as MainActivity).showSnackbarAbovePlayer(R.string.please_wait_for_data, Toast.LENGTH_LONG) -// return false -// } when (item.itemId) { R.id.visit_website_item -> if (feed.link != null) IntentUtils.openInBrowser(requireContext(), feed.link!!) R.id.share_item -> ShareUtils.shareFeedLink(requireContext(), feed) @@ -264,7 +249,6 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } @UnstableApi private fun reconnectLocalFolder(uri: Uri) { -// if (feed == null) return lifecycleScope.launch { try { withContext(Dispatchers.IO) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index 781855bd..0de5963b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -4,7 +4,6 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.CheckboxDoNotShowAgainBinding import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding import ac.mdiq.podcini.databinding.QueueFragmentBinding -import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed @@ -21,7 +20,9 @@ import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.* +import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.EpisodeUtil +import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils @@ -31,16 +32,13 @@ import ac.mdiq.podcini.ui.adapter.EpisodesAdapter import ac.mdiq.podcini.ui.adapter.SelectableAdapter import ac.mdiq.podcini.ui.dialog.ConfirmationDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog -import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.LiftOnScrollListener import ac.mdiq.podcini.ui.view.EpisodesRecyclerView import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder -import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences @@ -92,6 +90,9 @@ import java.util.* private var adapter: QueueRecyclerAdapter? = null private var curIndex = -1 + private var showBin: Boolean = false + private var addToQueueActionItem: SpeedDialActionItem? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true @@ -156,7 +157,7 @@ import java.util.* swipeRefreshLayout = binding.swipeRefresh swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance)) - swipeRefreshLayout.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext()) } +// swipeRefreshLayout.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext()) } emptyView = EmptyViewHandler(requireContext()) emptyView.attachToRecyclerView(recyclerView) @@ -169,7 +170,7 @@ import java.util.* speedDialView = multiSelectDial.fabSD speedDialView.overlayLayout = multiSelectDial.fabSDOverlay speedDialView.inflate(R.menu.episodes_apply_action_speeddial) - speedDialView.removeActionItemById(R.id.add_to_queue_batch) + addToQueueActionItem = speedDialView.removeActionItemById(R.id.add_to_queue_batch) speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener { override fun onMainActionSelected(): Boolean { return false @@ -182,8 +183,7 @@ import java.util.* } }) speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem -> - EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id) - .handleAction(adapter!!.selectedItems.filterIsInstance()) + EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(adapter!!.selectedItems.filterIsInstance()) adapter?.endSelectMode() true } @@ -260,6 +260,7 @@ import java.util.* private fun onQueueEvent(event: FlowEvent.QueueEvent) { Logd(TAG, "onQueueEvent() called with ${event.action.name}") + if (showBin) return if (adapter == null) { loadItems(true) return @@ -395,7 +396,8 @@ import java.util.* } private fun onPlayerStatusChanged(event: FlowEvent.PlayerSettingsEvent) { -// Logd(TAG, "onPlayerStatusChanged() called with event = [$event]") + if (showBin) return + // Logd(TAG, "onPlayerStatusChanged() called with event = [$event]") loadItems(false) refreshToolbarState() } @@ -403,8 +405,9 @@ import java.util.* private fun onEpisodePlayedEvent(event: FlowEvent.EpisodePlayedEvent) { // Sent when playback position is reset Logd(TAG, "onUnreadItemsChanged() called with event = [$event]") - if (event.episode == null) loadItems(false) - else { + if (event.episode == null) { + if (!showBin) loadItems(false) + } else { val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id) if (pos >= 0) queueItems[pos].setPlayed(event.episode.isPlayed()) } @@ -449,15 +452,29 @@ import java.util.* val keepSorted: Boolean = isQueueKeepSorted toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked) toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted) - toolbar.menu.findItem(R.id.switch_queue).setVisible(false) +// toolbar.menu.findItem(R.id.switch_queue).setVisible(false) + toolbar.menu.findItem(R.id.refresh_item).setVisible(false) } @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { val itemId = item.itemId when (itemId) { + R.id.show_bin -> { + showBin = !showBin + if (showBin) { + item.setIcon(R.drawable.ic_delete) + speedDialView.addActionItem(addToQueueActionItem) + swipeActions.detach() + } else { + item.setIcon(R.drawable.trash_can_arrow_up_solid) + speedDialView.removeActionItem(addToQueueActionItem) + swipeActions.attachTo(recyclerView) + } + loadItems(false) + } R.id.queue_lock -> toggleQueueLock() R.id.queue_sort -> QueueSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog") - R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) +// R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) R.id.clear_queue -> { // make sure the user really wants to clear the queue val conDialog: ConfirmationDialog = object : ConfirmationDialog(requireContext(), R.string.clear_queue_label, R.string.clear_queue_confirmation_msg) { @@ -468,6 +485,10 @@ import java.util.* } conDialog.createNewDialog().show() } + R.id.clear_bin -> { + curQueue.idsBin.clear() + upsertBlk(curQueue) {} + } R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) // R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show() else -> return false @@ -594,14 +615,17 @@ import java.util.* loadItemsRunning = true Logd(TAG, "loadItems() called") while (curQueue.name.isEmpty()) runBlocking { delay(100) } - curQueue.episodes.clear() - curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) - .find().sortedBy { curQueue.episodeIds.indexOf(it.id) })) - if (queueItems.isEmpty()) emptyView.hide() - queueItems.clear() - queueItems.addAll(curQueue.episodes) + if (showBin) { + queueItems.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.idsBin) + .find().sortedBy { curQueue.idsBin.indexOf(it.id) })) + } else { + curQueue.episodes.clear() + curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) + .find().sortedBy { curQueue.episodeIds.indexOf(it.id) })) + queueItems.addAll(curQueue.episodes) + } binding.progressBar.visibility = View.GONE // adapter?.setDummyViews(0) adapter?.updateItems(queueItems) diff --git a/app/src/main/res/drawable/trash_can_arrow_up_solid.xml b/app/src/main/res/drawable/trash_can_arrow_up_solid.xml new file mode 100644 index 00000000..931da865 --- /dev/null +++ b/app/src/main/res/drawable/trash_can_arrow_up_solid.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/feedinfo.xml b/app/src/main/res/layout/feedinfo.xml index 86ab232a..59120679 100644 --- a/app/src/main/res/layout/feedinfo.xml +++ b/app/src/main/res/layout/feedinfo.xml @@ -31,11 +31,6 @@ app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.6" /> - - + + @@ -69,6 +69,40 @@ android:orientation="vertical" android:paddingHorizontal="@dimen/additional_horizontal_spacing"> + + + + + + + + - - - -