diff --git a/app/build.gradle b/app/build.gradle index 829a8771..7d9af90b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -125,8 +125,8 @@ android { buildConfig true } defaultConfig { - versionCode 3020203 - versionName "6.0.3" + versionCode 3020204 + versionName "6.0.4" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt index b128c3ea..9250b610 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -720,7 +720,6 @@ object UserPreferences { /** * Returns the sort order for the queue keep sorted mode. * Note: This value is stored independently from the keep sorted state. - * * @see .isQueueKeepSorted */ get() { @@ -729,7 +728,6 @@ object UserPreferences { } /** * Sets the sort order for the queue keep sorted mode. - * * @see .setQueueKeepSorted */ set(sortOrder) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt index 23bd61c4..931505d9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt @@ -45,7 +45,7 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() { findPreference(UserPreferences.PREF_SHOW_TIME_LEFT)?.setOnPreferenceChangeListener { _: Preference?, newValue: Any? -> setShowRemainTimeSetting(newValue as Boolean?) // TODO: need another event type? - EventFlow.postEvent(FlowEvent.EpisodePlayedEvent()) +// EventFlow.postEvent(FlowEvent.EpisodePlayedEvent()) EventFlow.postEvent(FlowEvent.PlayerSettingsEvent()) true } 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 71be3ed4..4bd6ec68 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 @@ -249,19 +249,21 @@ object Episodes { fun addToHistory(episode: Episode, date: Date? = Date()) : Job { Logd(TAG, "addToHistory called") return runOnIOScope { - episode.media?.playbackCompletionDate = date - upsert(episode) {} + upsert(episode) { + it.media?.playbackCompletionDate = date + } EventFlow.postEvent(FlowEvent.HistoryEvent()) } } @JvmStatic fun setFavorite(episode: Episode, stat: Boolean) : Job { - Logd(TAG, "setFavorite called") + Logd(TAG, "setFavorite called $stat") return runOnIOScope { - episode.isFavorite = stat - upsert(episode) {} - EventFlow.postEvent(FlowEvent.FavoritesEvent(episode)) + val result = upsert(episode) { + it.isFavorite = stat + } + EventFlow.postEvent(FlowEvent.FavoritesEvent(result)) } } @@ -276,11 +278,12 @@ object Episodes { Logd(TAG, "markPlayed called") return runOnIOScope { for (episode in episodes) { - episode.playState = played - if (resetMediaPosition) episode.media?.setPosition(0) - upsert(episode) {} + val result = upsert(episode) { + it.playState = played + if (resetMediaPosition) it.media?.setPosition(0) + } if (played == Episode.PLAYED) removeFromAllQueues(episode) - EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(episode)) + EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result)) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index 0bc850f5..071846c6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -36,6 +36,7 @@ object Feeds { private val feedMap: MutableMap = mutableMapOf() private val tags: MutableList = mutableListOf() + @Synchronized fun getFeedList(): List { return feedMap.values.toList() } @@ -44,6 +45,7 @@ object Feeds { return tags } + @Synchronized fun updateFeedMap(feeds: List = listOf(), wipe: Boolean = false) { Logd(TAG, "updateFeedMap called feeds: ${feeds.size} wipe: $wipe") when { @@ -65,13 +67,17 @@ object Feeds { fun buildTags() { val tagsSet = mutableSetOf() - val feedsCopy = feedMap.values + val feedsCopy = synchronized(feedMap) { feedMap.values.toList() } for (feed in feedsCopy) { if (feed.preferences != null) tagsSet.addAll(feed.preferences!!.tags.filter { it != TAG_ROOT }) } - tags.clear() - tags.addAll(tagsSet) - tags.sort() + val newTags = tagsSet.intersect(tags.toSet()) + if (newTags.isNotEmpty()) { + tags.clear() + tags.addAll(tagsSet) + tags.sort() + EventFlow.postEvent(FlowEvent.FeedTagsChangedEvent()) + } } fun monitorFeeds() { @@ -90,8 +96,8 @@ object Feeds { Logd(TAG, "monitorFeeds inserted feed: ${changes.list[i].title}") updateFeedMap(listOf(changes.list[i])) monitorFeed(changes.list[i]) - EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED, changes.list[i].id)) } + EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED)) } // changes.changes.isNotEmpty() -> { // for (i in changes.changes) { @@ -456,12 +462,20 @@ object Feeds { // remove from queues removeFromAllQueuesQuiet(eids) // remove media files - deleteMediaFilesQuiet(context, feed.episodes) +// deleteMediaFilesQuiet(context, feed.episodes) realm.write { val feed_ = query(Feed::class).query("id == $0", feedId).first().find() if (feed_ != null) { val episodes = feed_.episodes.toList() - if (episodes.isNotEmpty()) episodes.forEach { e -> delete(e) } + if (episodes.isNotEmpty()) episodes.forEach { episode -> + val url = episode.media?.fileUrl + when { + // Local feed + url != null && url.startsWith("content://") -> DocumentFile.fromSingleUri(context, Uri.parse(url))?.delete() + url != null -> File(url).delete() + } + delete(episode) + } val feedToDelete = findLatest(feed_) if (feedToDelete != null) { delete(feedToDelete) @@ -475,28 +489,29 @@ object Feeds { } } - private fun deleteMediaFilesQuiet(context: Context, episodes: List) { - for (episode in episodes) { - val media = episode.media ?: continue - val url = media.fileUrl - when { - url != null && url.startsWith("content://") -> { - // Local feed - val documentFile = DocumentFile.fromSingleUri(context, Uri.parse(media.fileUrl)) - documentFile?.delete() - episode.media?.fileUrl = null - } - url != null -> { - // delete downloaded media file - val mediaFile = File(url) - mediaFile.delete() - episode.media?.downloaded = false - episode.media?.fileUrl = null - episode.media?.hasEmbeddedPicture = false - } - } - } - } +// private fun deleteMediaFilesQuiet(context: Context, episodes: List) { +// for (episode in episodes) { +// val media = episode.media ?: continue +// val url = media.fileUrl +// when { +// url != null && url.startsWith("content://") -> { +// // Local feed +// val documentFile = DocumentFile.fromSingleUri(context, Uri.parse(media.fileUrl)) +// documentFile?.delete() +//// episode.media?.fileUrl = null +// } +// url != null -> { +// // delete downloaded media file +// val mediaFile = File(url) +// mediaFile.delete() +//// since deleting entire feed, these are not necessary +//// episode.media?.downloaded = false +//// episode.media?.fileUrl = null +//// episode.media?.hasEmbeddedPicture = false +// } +// } +// } +// } @JvmStatic fun shouldAutoDeleteItemsOnFeed(feed: Feed): Boolean { 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 e4c2f274..c2ec6232 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 = 4L + private const val SCHEMA_VERSION_NUMBER = 6L private val ioScope = CoroutineScope(Dispatchers.IO) @@ -93,21 +93,24 @@ object RealmDB { Logd(TAG, "${caller?.className}.${caller?.methodName} upsert: ${entity.javaClass.simpleName}") } return realm.write { + var result: T = entity if (entity.isManaged()) { - findLatest(entity)?.let { + result = findLatest(entity)?.let { block(it) - } + it + } ?: entity } else { try { - copyToRealm(entity, UpdatePolicy.ALL).let { + result = copyToRealm(entity, UpdatePolicy.ALL).let { block(it) + it } } catch (e: Exception) { Log.e(TAG, "copyToRealm error: ${e.message}") showStackTrace() } } - entity + result } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt index 4d535ba6..7b21b37b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt @@ -4,6 +4,7 @@ import ac.mdiq.podcini.storage.utils.FeedFunding.Companion.extractPaymentLinks import ac.mdiq.podcini.storage.utils.EpisodeFilter import ac.mdiq.podcini.storage.utils.FeedFunding import ac.mdiq.podcini.storage.utils.SortOrder +import ac.mdiq.podcini.storage.utils.SortOrder.Companion.fromCode import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.types.RealmList @@ -126,6 +127,7 @@ class Feed : RealmObject { @Ignore var sortOrder: SortOrder? = null + get() = fromCode(preferences?.sortOrderCode ?: 0) set(value) { if (value == null) return field = value 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 b78b21b0..cfbd1477 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 @@ -23,7 +23,8 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val fun handleAction(items: List) { when (actionId) { - R.id.add_to_favorite_batch -> markFavorite(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.remove_from_queue_batch -> removeFromQueueChecked(items) R.id.mark_read_batch -> { @@ -68,7 +69,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val // showMessage(R.plurals.marked_unread_batch_label, items.size) // } - private fun markFavorite(items: List) { + private fun markFavorite(items: List, stat: Boolean) { for (item in items) { Episodes.setFavorite(item, true) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/menuhandler/EpisodeMenuHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/menuhandler/EpisodeMenuHandler.kt index c2181893..17ed092c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/menuhandler/EpisodeMenuHandler.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/menuhandler/EpisodeMenuHandler.kt @@ -48,7 +48,6 @@ object EpisodeMenuHandler { /** * This method should be called in the prepare-methods of menus. It changes * the visibility of the menu items depending on a FeedItem's attributes. - * * @param menu An instance of Menu * @param selectedItem The FeedItem for which the menu is supposed to be prepared * @return Returns true if selectedItem is not null. @@ -101,7 +100,6 @@ object EpisodeMenuHandler { */ private fun setItemVisibility(menu: Menu?, menuId: Int, visibility: Boolean) { if (menu == null) return - val item = menu.findItem(menuId) item?.setVisible(visibility) } @@ -120,26 +118,21 @@ object EpisodeMenuHandler { /** * The same method as [.onPrepareMenu], but lets the * caller also specify a list of menu items that should not be shown. - * * @param excludeIds Menu item that should be excluded * @return true if selectedItem is not null. */ @UnstableApi fun onPrepareMenu(menu: Menu?, selectedItem: Episode?, vararg excludeIds: Int): Boolean { if (menu == null || selectedItem == null) return false - val rc = onPrepareMenu(menu, selectedItem) if (rc && excludeIds.isNotEmpty()) { - for (id in excludeIds) { - setItemVisibility(menu, id, false) - } + for (id in excludeIds) setItemVisibility(menu, id, false) } return rc } /** * Default menu handling for the given FeedItem. - * * A Fragment instance, (rather than the more generic Context), is needed as a parameter * to support some UI operations, e.g., creating a Snackbar. */ @@ -204,14 +197,12 @@ object EpisodeMenuHandler { return false } } - // Refresh menu state return true } /** * Remove new flag with additional UI logic to allow undo with Snackbar. - * * Undo is useful for Remove new flag, given there is no UI to undo it otherwise * ,i.e., there is (context) menu item for add new flag */ @@ -219,7 +210,7 @@ object EpisodeMenuHandler { fun markReadWithUndo(fragment: Fragment, item: Episode?, playState: Int, showSnackbar: Boolean) { if (item == null) return - Logd(TAG, "markReadWithUndo(" + item.id + ")") + Logd(TAG, "markReadWithUndo( ${item.id} )") // we're marking it as unplayed since the user didn't actually play it // but they don't want it considered 'NEW' anymore markPlayed(playState, false, item) @@ -228,8 +219,7 @@ object EpisodeMenuHandler { val r = Runnable { val media: EpisodeMedia? = item.media val shouldAutoDelete: Boolean = if (item.feed == null) false else shouldAutoDeleteItemsOnFeed(item.feed!!) - if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) - deleteMediaOfEpisode(fragment.requireContext(), item) + if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) deleteMediaOfEpisode(fragment.requireContext(), item) } val playStateStringRes: Int = when (playState) { Episode.UNPLAYED -> if (item.playState == Episode.NEW) R.string.removed_inbox_label //was new @@ -252,8 +242,4 @@ object EpisodeMenuHandler { h.postDelayed(r, ceil((duration * 1.05f).toDouble()).toInt().toLong()) } - - fun removeNewFlagWithUndo(fragment: Fragment, item: Episode?) { - markReadWithUndo(fragment, item, Episode.UNPLAYED, false) - } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt index 4090ecd3..58b8ffba 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt @@ -152,7 +152,6 @@ open class EpisodesAdapter(mainActivity: MainActivity) * Instead, we tell the adapter to use partial binding by calling [.notifyItemChanged]. * We actually ignore the payload and always do a full bind but calling the partial bind method ensures * that ViewHolders are always re-used. - * * @param position Position of the item that has changed */ fun notifyItemChangedCompat(position: Int) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt index d8abb764..8583a438 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt @@ -25,7 +25,6 @@ class ShareDialog : BottomSheetDialogFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { ctx = requireContext() - item = requireArguments().getSerializable(ARGUMENT_FEED_ITEM) as Episode? prefs = requireActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) _binding = ShareEpisodeDialogBinding.inflate(inflater) @@ -93,7 +92,7 @@ class ShareDialog : BottomSheetDialogFragment() { } companion object { - private const val ARGUMENT_FEED_ITEM = "feedItem" +// private const val ARGUMENT_FEED_ITEM = "feedItem" private const val PREF_NAME = "ShareDialog" private const val PREF_SHARE_EPISODE_START_AT = "prefShareEpisodeStartAt" private const val PREF_SHARE_EPISODE_TYPE = "prefShareEpisodeType" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt index 50243b74..189d67ce 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt @@ -78,7 +78,7 @@ class TagSettingsDialog : DialogFragment() { addTag(binding.newTagEditText.text.toString().trim { it <= ' ' }) updatePreferencesTags(commonTags) buildTags() - EventFlow.postEvent(FlowEvent.FeedTagsChangedEvent()) +// EventFlow.postEvent(FlowEvent.FeedTagsChangedEvent()) } dialog.setNegativeButton(R.string.cancel_label, null) return dialog.create() @@ -113,7 +113,7 @@ class TagSettingsDialog : DialogFragment() { feed.preferences!!.tags.removeAll(commonTags) feed.preferences!!.tags.addAll(displayedTags) persistFeedPreferences(feed) -// EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed.id)) +// EventFlow.postEvent(FlowEvent.FeedPrefsChangeEvent(feed)) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt index 1794a037..b9af9ed8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt @@ -62,6 +62,7 @@ import kotlin.math.min override fun loadData(): List { allEpisodes = getEpisodes(0, Int.MAX_VALUE, getFilter(), allEpisodesSortOrder, false) + if (allEpisodes.isEmpty()) return listOf() return allEpisodes.subList(0, min(allEpisodes.size-1, page * EPISODES_PER_PAGE)) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 62dc7e68..2e0fa70d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -13,6 +13,7 @@ import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocal import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive +import ac.mdiq.podcini.playback.base.InTheatre.curEpisode import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed @@ -22,6 +23,7 @@ import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode import ac.mdiq.podcini.receiver.MediaButtonReceiver +import ac.mdiq.podcini.storage.database.RealmDB.unmanagedCopy import ac.mdiq.podcini.storage.model.Chapter import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.Playable @@ -187,11 +189,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar playerUI?.butPlay?.setIsShowPlay(isShowPlay) } - private fun setChapterDividers(media: Playable?) { - if (media == null) return + private fun setChapterDividers() { + if (currentMedia == null) return - if (media.getChapters().isNotEmpty()) { - val chapters: List = media.getChapters() + if (currentMedia!!.getChapters().isNotEmpty()) { + val chapters: List = currentMedia!!.getChapters() val dividerPos = FloatArray(chapters.size) for (i in chapters.indices) { @@ -213,22 +215,19 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar return } if (!actMain.isPlayerVisible()) actMain.setPlayerVisible(true) - if (!isCollapsed && (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier())) playerDetailsFragment?.updateInfo() if (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier() || (includingChapters && !curMedia!!.chaptersLoaded())) { Logd(TAG, "loadMediaInfo loading details ${curMedia?.getIdentifier()} chapter: $includingChapters") lifecycleScope.launch { - val media: Playable = withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { curMedia!!.apply { if (includingChapters) ChapterUtils.loadChapters(this, requireContext(), false) } } - currentMedia = media - if (currentMedia is EpisodeMedia) { - val item = (currentMedia as EpisodeMedia).episode - if (item != null) playerDetailsFragment?.setItem(item) - } + currentMedia = curMedia + val item = (currentMedia as? EpisodeMedia)?.episode + if (item != null) playerDetailsFragment?.setItem(item) updateUi() playerUI?.updateUi(currentMedia) // TODO: disable for now @@ -263,8 +262,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar private fun updateUi() { Logd(TAG, "updateUi called") - setChapterDividers(currentMedia) - setupOptionsMenu(currentMedia) + setChapterDividers() + setupOptionsMenu() } override fun onCreate(savedInstanceState: Bundle?) { @@ -339,10 +338,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar playerUI?.onPlaybackServiceChanged(event) } is FlowEvent.PlayEvent -> onEvenStartPlay(event) + is FlowEvent.FavoritesEvent -> onFavoriteEvent(event) is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event) - is FlowEvent.FavoritesEvent -> loadMediaInfo(false) is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false) - is FlowEvent.PlaybackPositionEvent -> onPositionUpdate(event) + is FlowEvent.PlaybackPositionEvent -> playerUI?.onPositionUpdate(event) is FlowEvent.SpeedChangedEvent -> playerUI?.updatePlaybackSpeedButton(event) else -> {} } @@ -350,9 +349,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } } - private fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) { -// if (!isCollapsed) loadMediaInfo(false) - playerUI?.onPositionUpdate(event) + private fun onFavoriteEvent(event: FlowEvent.FavoritesEvent) { + if (curEpisode?.id == event.episode.id) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, event.episode) } override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { @@ -409,12 +407,12 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar ?.start() } - private fun setupOptionsMenu(media: Playable?) { + private fun setupOptionsMenu() { if (toolbar.menu.size() == 0) toolbar.inflateMenu(R.menu.mediaplayer) - val isEpisodeMedia = media is EpisodeMedia + val isEpisodeMedia = currentMedia is EpisodeMedia toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia) - val item = if (isEpisodeMedia) (media as EpisodeMedia).episode else null + val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episode else null EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item) val mediaType = curMedia?.getMediaType() 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 c04f3d2f..23cbc32c 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 @@ -130,7 +130,6 @@ import kotlinx.coroutines.flow.collectLatest override fun onMainActionSelected(): Boolean { return false } - override fun onToggleChanged(open: Boolean) { if (open && listAdapter.selectedCount == 0) { (activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT) @@ -147,9 +146,8 @@ import kotlinx.coroutines.flow.collectLatest R.id.mark_unread_batch -> confirmationString = R.string.multi_select_mark_unplayed_confirmation } } - if (confirmationString == 0) { - performMultiSelectAction(actionItem.id) - } else { + if (confirmationString == 0) performMultiSelectAction(actionItem.id) + else { object : ConfirmationDialog(activity as MainActivity, R.string.multi_select, confirmationString) { override fun onConfirmButtonPressed(dialog: DialogInterface) { performMultiSelectAction(actionItem.id) @@ -158,7 +156,6 @@ import kotlinx.coroutines.flow.collectLatest } true } - return binding.root } @@ -384,7 +381,7 @@ import kotlinx.coroutines.flow.collectLatest Logd(TAG, "Received event: ${event.TAG}") when (event) { is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() - is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> loadItems() + is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent, is FlowEvent.FavoritesEvent -> loadItems() is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event) is FlowEvent.EpisodeEvent -> onEpisodeEvent(event) else -> {} 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 4e1a195e..edb02bb2 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 @@ -59,7 +59,7 @@ import java.util.* private val binding get() = _binding!! private var runningDownloads: Set = HashSet() - private var items: MutableList = mutableListOf() + private var episodes: MutableList = mutableListOf() private lateinit var adapter: DownloadsListAdapter private lateinit var toolbar: MaterialToolbar @@ -124,7 +124,6 @@ import java.util.* override fun onMainActionSelected(): Boolean { return false } - override fun onToggleChanged(open: Boolean) { if (open && adapter.selectedCount == 0) { (activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT) @@ -143,7 +142,6 @@ import java.util.* DownloadLogFragment().show(childFragmentManager, null) addEmptyView() - return binding.root } @@ -195,7 +193,7 @@ import java.util.* return // Refreshed anyway } for (downloadUrl in event.urls) { - val pos = EpisodeUtil.indexOfItemWithDownloadUrl(items.toList(), downloadUrl) + val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl) if (pos >= 0) adapter.notifyItemChangedCompat(pos) } } @@ -215,7 +213,11 @@ import java.util.* when (event) { is FlowEvent.EpisodeEvent -> onEpisodeEvent(event) is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event) - is FlowEvent.PlayerSettingsEvent, is FlowEvent.DownloadLogEvent, is FlowEvent.EpisodePlayedEvent -> loadItems() + is FlowEvent.FavoritesEvent -> onFavoriteEvent(event) + is FlowEvent.PlayerSettingsEvent -> loadItems() + is FlowEvent.DownloadLogEvent -> loadItems() + is FlowEvent.EpisodePlayedEvent -> onEpisodePlayedEvent(event) + is FlowEvent.QueueEvent -> loadItems() is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event) else -> {} @@ -233,6 +235,27 @@ import java.util.* // } } + private fun onFavoriteEvent(event: FlowEvent.FavoritesEvent) { + val item = event.episode + val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) + if (pos >= 0) { + episodes.removeAt(pos) + episodes.add(pos, item) + adapter.notifyItemChangedCompat(pos) + } + } + + private fun onEpisodePlayedEvent(event: FlowEvent.EpisodePlayedEvent) { + if (event.episode == null) return + val item = event.episode + val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) + if (pos >= 0) { + episodes.removeAt(pos) + episodes.add(pos, item) + adapter.notifyItemChangedCompat(pos) + } + } + override fun onContextItemSelected(item: MenuItem): Boolean { val selectedItem: Episode? = adapter.longPressedItem if (selectedItem == null) { @@ -257,12 +280,12 @@ import java.util.* val size: Int = event.episodes.size while (i < size) { val item: Episode = event.episodes[i] - val pos = EpisodeUtil.indexOfItemWithId(items.toList(), item.id) + val pos = EpisodeUtil.indexOfItemWithId(episodes.toList(), item.id) if (pos >= 0) { - items.removeAt(pos) + episodes.removeAt(pos) val media = item.media if (media != null && media.downloaded) { - items.add(pos, item) + episodes.add(pos, item) // adapter.notifyItemChangedCompat(pos) } else { // adapter.notifyItemRemoved(pos) @@ -273,7 +296,7 @@ import java.util.* // have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash if (size > 0) { // adapter.setDummyViews(0) - adapter.updateItems(items) + adapter.updateItems(episodes) } refreshInfoBar() } @@ -304,16 +327,11 @@ import java.util.* emptyView.hide() lifecycleScope.launch { try { - val result = withContext(Dispatchers.IO) { - Logd(TAG, "loading") + withContext(Dispatchers.IO) { val sortOrder: SortOrder? = UserPreferences.downloadsSortedOrder -// val downloadedItems = realm.query(Episode::class).query("media.downloaded == true").find().toMutableList() -// if (sortOrder != null) getPermutor(sortOrder).reorder(downloadedItems) val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), sortOrder) - Logd(TAG, "downloadedItems: ${downloadedItems.size}") - if (runningDownloads.isEmpty()) downloadedItems + if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList() else { - Logd(TAG, "runningDownloads: ${runningDownloads.size}") val mediaUrls: MutableList = ArrayList() for (url in runningDownloads) { if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue @@ -321,14 +339,13 @@ import java.util.* } val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList() currentDownloads.addAll(downloadedItems) - currentDownloads + episodes = currentDownloads } } withContext(Dispatchers.Main) { - items = result.toMutableList() // adapter.setDummyViews(0) binding.progLoading.visibility = View.GONE - adapter.updateItems(result) + adapter.updateItems(episodes) refreshInfoBar() } } catch (e: Throwable) { @@ -352,10 +369,10 @@ import java.util.* } private fun refreshInfoBar() { - var info = String.format(Locale.getDefault(), "%d%s", items.size, getString(R.string.episodes_suffix)) - if (items.isNotEmpty()) { + var info = String.format(Locale.getDefault(), "%d%s", episodes.size, getString(R.string.episodes_suffix)) + if (episodes.isNotEmpty()) { var sizeMB: Long = 0 - for (item in items) sizeMB += item.media?.size ?: 0 + for (item in episodes) sizeMB += item.media?.size ?: 0 info += " • " + (sizeMB / 1000000) + " MB" } binding.infoBar.text = info diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 016599aa..684aa257 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -21,7 +21,6 @@ import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.utils.ThemeUtils -import ac.mdiq.podcini.ui.view.CircularProgressBar import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.DateFormatter @@ -75,7 +74,7 @@ import kotlin.math.max private var homeFragment: EpisodeHomeFragment? = null private var itemLoaded = false - private var item: Episode? = null + private var episode: Episode? = null private var webviewData: String? = null private lateinit var shownotesCleaner: ShownotesCleaner @@ -93,7 +92,6 @@ import kotlin.math.max super.onCreateView(inflater, container, savedInstanceState) _binding = EpisodeInfoFragmentBinding.inflate(inflater, container, false) -// root = binding.root Logd(TAG, "fragment onCreateView") toolbar = binding.toolbar @@ -108,7 +106,7 @@ import kotlin.math.max webvDescription = binding.webvDescription webvDescription.setTimecodeSelectedListener { time: Int? -> val cMedia = curMedia - if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) seekTo(time ?: 0) + if (episode?.media?.getIdentifier() == cMedia?.getIdentifier()) seekTo(time ?: 0) else (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position, Snackbar.LENGTH_LONG) } registerForContextMenu(webvDescription) @@ -119,10 +117,10 @@ import kotlin.math.max butAction2 = binding.butAction2 binding.homeButton.setOnClickListener { - if (!item?.link.isNullOrEmpty()) { - homeFragment = EpisodeHomeFragment.newInstance(item!!) + if (!episode?.link.isNullOrEmpty()) { + homeFragment = EpisodeHomeFragment.newInstance(episode!!) (activity as MainActivity).loadChildFragment(homeFragment!!) - } else Toast.makeText(context, "Episode link is not valid ${item?.link}", Toast.LENGTH_LONG).show() + } else Toast.makeText(context, "Episode link is not valid ${episode?.link}", Toast.LENGTH_LONG).show() } butAction1.setOnClickListener(View.OnClickListener { @@ -201,8 +199,8 @@ import kotlin.math.max @UnstableApi override fun onMenuItemClick(menuItem: MenuItem): Boolean { when (menuItem.itemId) { R.id.share_notes -> { - if (item == null) return false - val notes = item!!.description + if (episode == null) return false + val notes = episode!!.description if (!notes.isNullOrEmpty()) { val shareText = if (Build.VERSION.SDK_INT >= 24) HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() else HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() @@ -217,8 +215,8 @@ import kotlin.math.max return true } else -> { - if (item == null) return false - return EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, item!!) + if (episode == null) return false + return EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, episode!!) } } } @@ -234,44 +232,45 @@ import kotlin.math.max @OptIn(UnstableApi::class) override fun onDestroyView() { super.onDestroyView() Logd(TAG, "onDestroyView") - binding.root.removeView(webvDescription) webvDescription.clearHistory() webvDescription.clearCache(true) webvDescription.clearView() webvDescription.destroy() - _binding = null } @UnstableApi private fun onFragmentLoaded() { if (webviewData != null && !itemLoaded) webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData!!, "text/html", "utf-8", "about:blank") - // if (item?.link != null) binding.webView.loadUrl(item!!.link!!) updateAppearance() } + private fun prepareMenu() { + if (episode!!.media != null) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast) + // these are already available via button1 and button2 + else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item) + } + @UnstableApi private fun updateAppearance() { - if (item == null) { + if (episode == null) { Logd(TAG, "updateAppearance item is null") return } - if (item!!.media != null) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.open_podcast) - // these are already available via button1 and button2 - else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item) + prepareMenu() - if (item!!.feed != null) binding.txtvPodcast.text = item!!.feed!!.title - binding.txtvTitle.text = item!!.title - binding.itemLink.text = item!!.link + if (episode!!.feed != null) binding.txtvPodcast.text = episode!!.feed!!.title + binding.txtvTitle.text = episode!!.title + binding.itemLink.text = episode!!.link - if (item?.pubDate != null) { - val pubDateStr = DateFormatter.formatAbbrev(context, Date(item!!.pubDate)) + if (episode?.pubDate != null) { + val pubDateStr = DateFormatter.formatAbbrev(context, Date(episode!!.pubDate)) binding.txtvPublished.text = pubDateStr - binding.txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(Date(item!!.pubDate))) + binding.txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(Date(episode!!.pubDate))) } - val media = item?.media + val media = episode?.media when { media == null -> binding.txtvSize.text = "" media.size > 0 -> binding.txtvSize.text = Formatter.formatShortFileSize(activity, media.size) @@ -279,7 +278,7 @@ import kotlin.math.max binding.txtvSize.text = "{faw_spinner}" // Iconify.addIcons(size) lifecycleScope.launch { - val sizeValue = getMediaSize(item) + val sizeValue = getMediaSize(episode) if (sizeValue <= 0) binding.txtvSize.text = "" else binding.txtvSize.text = Formatter.formatShortFileSize(activity, sizeValue) } @@ -287,10 +286,10 @@ import kotlin.math.max else -> binding.txtvSize.text = "" } - val imgLocFB = ImageResourceUtils.getFallbackImageLocation(item!!) + val imgLocFB = ImageResourceUtils.getFallbackImageLocation(episode!!) val imageLoader = imgvCover.context.imageLoader val imageRequest = ImageRequest.Builder(requireContext()) - .data(item!!.imageLocation) + .data(episode!!.imageLocation) .placeholder(R.color.light_gray) .listener(object : ImageRequest.Listener { override fun onError(request: ImageRequest, result: ErrorResult) { @@ -313,21 +312,21 @@ import kotlin.math.max @UnstableApi private fun updateButtons() { binding.circularProgressBar.visibility = View.GONE val dls = DownloadServiceInterface.get() - if (item != null && item!!.media != null && item!!.media!!.downloadUrl != null) { - val url = item!!.media!!.downloadUrl!! + if (episode != null && episode!!.media != null && episode!!.media!!.downloadUrl != null) { + val url = episode!!.media!!.downloadUrl!! if (dls != null && dls.isDownloadingEpisode(url)) { binding.circularProgressBar.visibility = View.VISIBLE - binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), item) + binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), episode) binding.circularProgressBar.setIndeterminate(dls.isEpisodeQueued(url)) } } - val media: EpisodeMedia? = item?.media + val media: EpisodeMedia? = episode?.media if (media == null) { - if (item != null) { + if (episode != null) { // actionButton1 = VisitWebsiteActionButton(item!!) butAction1.visibility = View.INVISIBLE - actionButton2 = VisitWebsiteActionButton(item!!) + actionButton2 = VisitWebsiteActionButton(episode!!) } binding.noMediaLabel.visibility = View.VISIBLE } else { @@ -336,19 +335,19 @@ import kotlin.math.max binding.txtvDuration.text = Converter.getDurationStringLong(media.getDuration()) binding.txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong())) } - if (item != null) { + if (episode != null) { actionButton1 = when { - media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(item!!) - InTheatre.isCurrentlyPlaying(media) -> PauseActionButton(item!!) - item!!.feed != null && item!!.feed!!.isLocalFeed -> PlayLocalActionButton(item!!) - media.downloaded -> PlayActionButton(item!!) - else -> StreamActionButton(item!!) + media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(episode!!) + InTheatre.isCurrentlyPlaying(media) -> PauseActionButton(episode!!) + episode!!.feed != null && episode!!.feed!!.isLocalFeed -> PlayLocalActionButton(episode!!) + media.downloaded -> PlayActionButton(episode!!) + else -> StreamActionButton(episode!!) } actionButton2 = when { - media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(item!!) - dls != null && media.downloadUrl != null && dls.isDownloadingEpisode(media.downloadUrl!!) -> CancelDownloadActionButton(item!!) - !media.downloaded -> DownloadActionButton(item!!) - else -> DeleteActionButton(item!!) + media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(episode!!) + dls != null && media.downloadUrl != null && dls.isDownloadingEpisode(media.downloadUrl!!) -> CancelDownloadActionButton(episode!!) + !media.downloaded -> DownloadActionButton(episode!!) + else -> DeleteActionButton(episode!!) } // if (actionButton2 != null && media.getMediaType() == MediaType.FLASH) actionButton2!!.visibility = View.GONE } @@ -369,9 +368,9 @@ import kotlin.math.max } @OptIn(UnstableApi::class) private fun openPodcast() { - if (item?.feedId == null) return + if (episode?.feedId == null) return - val fragment: Fragment = FeedEpisodesFragment.newInstance(item!!.feedId!!) + val fragment: Fragment = FeedEpisodesFragment.newInstance(episode!!.feedId!!) (activity as MainActivity).loadChildFragment(fragment) } @@ -388,6 +387,8 @@ import kotlin.math.max EventFlow.events.collectLatest { event -> Logd(TAG, "Received event: ${event.TAG}") when (event) { + is FlowEvent.QueueEvent -> onQueueEvent(event) + is FlowEvent.FavoritesEvent -> onFavoriteEvent(event) is FlowEvent.EpisodeEvent -> onEpisodeEvent(event) is FlowEvent.PlayerSettingsEvent -> updateButtons() is FlowEvent.EpisodePlayedEvent -> load() @@ -406,11 +407,32 @@ import kotlin.math.max } } + private fun onFavoriteEvent(event: FlowEvent.FavoritesEvent) { + if (episode?.id == event.episode.id) { + episode = unmanagedCopy(event.episode) + prepareMenu() + } + } + + private fun onQueueEvent(event: FlowEvent.QueueEvent) { + var i = 0 + val size: Int = event.episodes.size + while (i < size) { + val item_ = event.episodes[i] + if (item_.id == episode?.id) { + episode = unmanagedCopy(item_) + prepareMenu() + break + } + i++ + } + } + private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) { // Logd(TAG, "onEventMainThread() called with ${event.TAG}") - if (this.item == null) return + if (this.episode == null) return for (item in event.episodes) { - if (this.item!!.id == item.id) { + if (this.episode!!.id == item.id) { load() return } @@ -418,8 +440,8 @@ import kotlin.math.max } private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) { - if (item == null || item!!.media == null) return - if (!event.urls.contains(item!!.media!!.downloadUrl)) return + if (episode == null || episode!!.media == null) return + if (!event.urls.contains(episode!!.media!!.downloadUrl)) return if (itemLoaded && activity != null) updateButtons() } @@ -429,17 +451,14 @@ import kotlin.math.max Logd(TAG, "load() called") lifecycleScope.launch { try { - val result = withContext(Dispatchers.IO) { - val feedItem = item - if (feedItem != null) { - val duration = feedItem.media?.getDuration()?: Int.MAX_VALUE - webviewData = shownotesCleaner.processShownotes(feedItem.description?:"", duration) + withContext(Dispatchers.IO) { + if (episode != null) { + val duration = episode!!.media?.getDuration()?: Int.MAX_VALUE + webviewData = shownotesCleaner.processShownotes(episode!!.description?:"", duration) } - feedItem } withContext(Dispatchers.Main) { binding.progbarLoading.visibility = View.GONE - item = result onFragmentLoaded() itemLoaded = true } @@ -450,7 +469,7 @@ import kotlin.math.max } fun setItem(item_: Episode) { - item = unmanagedCopy(item_) + episode = unmanagedCopy(item_) } companion object { @@ -498,7 +517,6 @@ import kotlin.math.max if (size <= 0) media.setCheckedOnSizeButUnknown() else media.size = size upsert(episode) {} - size } } 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 f9267f17..2237c304 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 @@ -183,8 +183,7 @@ import java.util.concurrent.Semaphore } }) dialBinding.fabSD.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 } @@ -331,7 +330,6 @@ import java.util.concurrent.Semaphore var i = 0 val size: Int = event.episodes.size -// feed = getFeed(feed!!.id) ?: error("Can't find latest for feed ${feed?.title}") while (i < size) { val item = event.episodes[i] if (item.feedId != feed!!.id) continue @@ -354,8 +352,11 @@ import java.util.concurrent.Semaphore if (item.feedId != feed!!.id) continue val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) if (pos >= 0) { - episodes[pos].playState = item.playState + episodes.removeAt(pos) + episodes.add(pos, item) adapter.notifyItemChangedCompat(pos) +// episodes[pos].playState = item.playState +// adapter.notifyItemChangedCompat(pos) } i++ } @@ -374,8 +375,11 @@ import java.util.concurrent.Semaphore val item = event.episode val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) if (pos >= 0) { - episodes[pos].playState = item.playState + episodes.removeAt(pos) + episodes.add(pos, item) adapter.notifyItemChangedCompat(pos) +// episodes[pos].playState = item.playState +// adapter.notifyItemChangedCompat(pos) } } @@ -383,8 +387,11 @@ import java.util.concurrent.Semaphore val item = event.episode val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) if (pos >= 0) { - episodes[pos].isFavorite = item.isFavorite + episodes.removeAt(pos) + episodes.add(pos, item) adapter.notifyItemChangedCompat(pos) +// episodes[pos].isFavorite = item.isFavorite +// adapter.notifyItemChangedCompat(pos) } } @@ -396,9 +403,6 @@ import java.util.concurrent.Semaphore val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl) if (pos >= 0) { // TODO: need a better way - val item = episodes[pos] -// item.media?.downloaded = true - Logd(TAG, "onEpisodeDownloadEvent ${item.title}") adapter.notifyItemChangedCompat(pos) } } @@ -704,8 +708,7 @@ import java.util.concurrent.Semaphore class SingleFeedSortDialog(val feed: Feed?) : EpisodeSortDialog() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - sortOrder = if (feed?.sortOrder == null) SortOrder.DATE_NEW_OLD - else feed.sortOrder + sortOrder = feed?.sortOrder ?: SortOrder.DATE_NEW_OLD } override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) { if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.PLAYED_DATE_OLD_NEW || ascending == SortOrder.COMPLETED_DATE_OLD_NEW diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt index 9f259dbc..7cba0cf2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt @@ -152,6 +152,7 @@ import kotlin.math.min override fun loadData(): List { allHistory = getHistory(0, Int.MAX_VALUE, startDate, endDate, sortOrder).toMutableList() + if (allHistory.isEmpty()) return listOf() return allHistory.subList(0, min(allHistory.size-1, page * EPISODES_PER_PAGE)) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index 19648e2e..824e490d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -408,10 +408,6 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { val numFeeds = getFeedList().size while (curQueue.name.isEmpty()) runBlocking { delay(100) } val queueSize = curQueue.episodeIds.size -// if (queueSize == 0) { -// val queue = realm.query(PlayQueue::class).sort("updated", Sort.DESCENDING).first().find() -// queueSize = queue?.episodeIds?.size ?: 0 -// } Logd(TAG, "getDatasetStats: queueSize: $queueSize") return DatasetStats(queueSize, numDownloadedItems, AutoCleanups.build().getReclaimableItems(), numItems, numFeeds) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index 8cbf687e..7b1a9ece 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -75,12 +75,16 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec private lateinit var toolbar: MaterialToolbar private lateinit var speedDialView: SpeedDialView + private lateinit var catAdapter: ArrayAdapter + private var tagFilterIndex = 1 +// TODO: currently not used private var displayedFolder: String = "" private var displayUpArrow = false private var feedList: MutableList = mutableListOf() private var feedListFiltered: List = mutableListOf() + private val tags: MutableList = mutableListOf() private var useGrid: Boolean? = null @@ -121,12 +125,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec resetTags() - val catAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, tags) + catAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, tags) catAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - val catSpinner = binding.categorySpinner - catSpinner.setAdapter(catAdapter) - catSpinner.setSelection(catAdapter.getPosition("All")) - catSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + binding.categorySpinner.setAdapter(catAdapter) + binding.categorySpinner.setSelection(catAdapter.getPosition("All")) + binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { tagFilterIndex = position filterOnTag() @@ -192,6 +195,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } override fun onStart() { + Logd(TAG, "onStart()") super.onStart() initAdapter() procFlowEvents() @@ -231,6 +235,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec listAdapter.setItems(feedListFiltered) } + private fun resetTags() { + tags.clear() + tags.add("Untagged") + tags.add("All") + tags.addAll(getTags()) + } + private var eventSink: Job? = null private var eventStickySink: Job? = null private fun cancelFlowEvents() { @@ -244,9 +255,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec EventFlow.events.collectLatest { event -> Logd(TAG, "Received event: ${event.TAG}") when (event) { - is FlowEvent.FeedListEvent -> onFeedListChanged(event) + is FlowEvent.FeedListEvent -> loadSubscriptions() is FlowEvent.EpisodePlayedEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions() - is FlowEvent.FeedTagsChangedEvent -> resetTags() + is FlowEvent.FeedTagsChangedEvent -> loadSubscriptions() else -> {} } } @@ -267,7 +278,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec when (itemId) { R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) R.id.subscriptions_sort -> FeedSortDialog.showDialog(requireContext()) -// R.id.subscriptions_filter -> SubscriptionsFilterDialog().show(childFragmentManager, "filter") R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) else -> return false } @@ -286,15 +296,14 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec emptyView.hide() lifecycleScope.launch { try { - val result = withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { + feedList = getFeedList().toMutableList() sortFeeds() - val fList: List = getFeedList() - fList + resetTags() } withContext(Dispatchers.Main) { // We have fewer items. This can result in items being selected that are no longer visible. - if ( feedListFiltered.size > result.size) listAdapter.endSelectMode() - feedList = result.toMutableList() + if ( feedListFiltered.size > feedList.size) listAdapter.endSelectMode() filterOnTag() binding.progressBar.visibility = View.GONE listAdapter.setItems(feedListFiltered) @@ -374,7 +383,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec comparator(counterMap) } } - feedList.sortWith(comparator) + synchronized(feedList) { feedList.sortWith(comparator) } } private fun counterMap(episodes: RealmResults): Map { @@ -411,12 +420,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec return FeedMenuHandler.onMenuItemClicked(this, item.itemId, feed) { this.loadSubscriptions() } } - private fun onFeedListChanged(event: FlowEvent.FeedListEvent) { -// val feeds_ = realm.query(Feed::class,"id IN $0", event.feedIds).find() -// updateFeedMap(feeds_) - loadSubscriptions() - } - override fun onEndSelectMode() { speedDialView.close() speedDialView.visibility = View.GONE @@ -425,10 +428,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec override fun onStartSelectMode() { speedDialView.visibility = View.VISIBLE - val feedsOnly: MutableList = ArrayList() - for (item in feedListFiltered) { - feedsOnly.add(item) - } + val feedsOnly: MutableList = ArrayList(feedListFiltered) +// feedsOnly.addAll(feedListFiltered) listAdapter.setItems(feedsOnly) } @@ -437,9 +438,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec fun handleAction(id: Int) { when (id) { R.id.remove_feed -> RemoveFeedDialog.show(activity, selectedItems) -// R.id.notify_new_episodes -> { -// notifyNewEpisodesPrefHandler() -// } R.id.keep_updated -> keepUpdatedPrefHandler() R.id.autodownload -> autoDownloadPrefHandler() R.id.autoDeleteDownload -> autoDeleteEpisodesPrefHandler() @@ -458,22 +456,22 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec preferenceSwitchDialog.openDialog() } @UnstableApi private fun playbackSpeedPrefHandler() { - val viewBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater) - viewBinding.seekBar.setProgressChangedListener { speed: Float? -> - viewBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed) + val vBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater) + vBinding.seekBar.setProgressChangedListener { speed: Float? -> + vBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed) } - viewBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> - viewBinding.seekBar.isEnabled = !isChecked - viewBinding.seekBar.alpha = if (isChecked) 0.4f else 1f - viewBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f + vBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + vBinding.seekBar.isEnabled = !isChecked + vBinding.seekBar.alpha = if (isChecked) 0.4f else 1f + vBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f } - viewBinding.seekBar.updateSpeed(1.0f) + vBinding.seekBar.updateSpeed(1.0f) MaterialAlertDialogBuilder(activity) .setTitle(R.string.playback_speed) - .setView(viewBinding.root) + .setView(vBinding.root) .setPositiveButton("OK") { _: DialogInterface?, _: Int -> - val newSpeed = if (viewBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL - else viewBinding.seekBar.currentSpeed + val newSpeed = if (vBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL + else vBinding.seekBar.currentSpeed saveFeedPreferences { feedPreferences: FeedPreferences -> feedPreferences.playSpeed = newSpeed } @@ -522,7 +520,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec @OptIn(UnstableApi::class) private abstract inner class SubscriptionsAdapter : SelectableAdapter(activity as MainActivity), View.OnCreateContextMenuListener { - protected var feedList: List var selectedItem: Feed? = null protected var longPressedPosition: Int = 0 // used to init actionMode @@ -701,29 +698,25 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec private inner class ViewHolderExpanded(itemView: View) : RecyclerView.ViewHolder(itemView) { val binding = SubscriptionItemBinding.bind(itemView) val count: TextView = binding.countLabel - val coverImage: ImageView = binding.coverImage val infoCard: LinearLayout = binding.infoCard val selectView: FrameLayout = binding.selectContainer val selectCheckbox: CheckBox = binding.selectCheckBox - private val errorIcon: View = binding.errorIcon - fun bind(drawerItem: Feed) { + fun bind(feed: Feed) { val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context, R.drawable.ic_checkbox_background) selectView.background = drawable // Setting this in XML crashes API <= 21 - binding.titleLabel.text = drawerItem.title - binding.producerLabel.text = drawerItem.author - coverImage.contentDescription = drawerItem.title + binding.titleLabel.text = feed.title + binding.producerLabel.text = feed.author + coverImage.contentDescription = feed.title coverImage.setImageDrawable(null) - val counter = drawerItem.episodes.size - count.text = NumberFormat.getInstance().format(counter.toLong()) + " episodes" + count.text = NumberFormat.getInstance().format(feed.episodes.size.toLong()) + " episodes" count.visibility = View.VISIBLE val mainActRef = (activity as MainActivity) val coverLoader = CoverLoader(mainActRef) - val feed: Feed = drawerItem coverLoader.withUri(feed.imageUrl) errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE @@ -755,20 +748,18 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec private val errorIcon: View = binding.errorIcon - fun bind(drawerItem: Feed) { + fun bind(feed: Feed) { val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context, R.drawable.ic_checkbox_background) selectView.background = drawable // Setting this in XML crashes API <= 21 - title.text = drawerItem.title - coverImage.contentDescription = drawerItem.title + title.text = feed.title + coverImage.contentDescription = feed.title coverImage.setImageDrawable(null) - val counter = drawerItem.episodes.size - count.text = NumberFormat.getInstance().format(counter.toLong()) + count.text = NumberFormat.getInstance().format(feed.episodes.size.toLong()) count.visibility = View.VISIBLE val mainActRef = (activity as MainActivity) val coverLoader = CoverLoader(mainActRef) - val feed: Feed = drawerItem coverLoader.withUri(feed.imageUrl) errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE @@ -800,30 +791,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } } - companion object { - val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous" - - private const val KEY_UP_ARROW = "up_arrow" - private const val ARGUMENT_FOLDER = "folder" - - private val tags: MutableList = mutableListOf() - - fun newInstance(folderTitle: String?): SubscriptionsFragment { - val fragment = SubscriptionsFragment() - val args = Bundle() - args.putString(ARGUMENT_FOLDER, folderTitle) - fragment.arguments = args - return fragment - } - - fun resetTags() { - tags.clear() - tags.add("Untagged") - tags.add("All") - tags.addAll(getTags()) - } - } - class PreferenceListDialog(private var context: Context, private val title: String) { private var onPreferenceChangedListener: OnPreferenceChangedListener? = null private var selectedPos = 0 @@ -882,4 +849,19 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec this.onPreferenceChangedListener = onPreferenceChangedListener } } + + companion object { + val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous" + + private const val KEY_UP_ARROW = "up_arrow" + private const val ARGUMENT_FOLDER = "folder" + + fun newInstance(folderTitle: String?): SubscriptionsFragment { + val fragment = SubscriptionsFragment() + val args = Bundle() + args.putString(ARGUMENT_FOLDER, folderTitle) + fragment.arguments = args + return fragment + } + } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt index 165298ab..05f4f68b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt @@ -90,6 +90,7 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro } fun bind(item: Episode) { +// Logd(TAG, "in bind: ${item.title} ${item.isFavorite} ${item.isPlayed()}") this.episode = item placeholder.text = item.feed?.title title.text = item.title diff --git a/app/src/main/res/menu/episodes_apply_action_speeddial.xml b/app/src/main/res/menu/episodes_apply_action_speeddial.xml index 0f27f099..1787e34e 100644 --- a/app/src/main/res/menu/episodes_apply_action_speeddial.xml +++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml @@ -42,4 +42,9 @@ android:icon="@drawable/ic_star" android:title="@string/add_to_favorite_label" /> + + diff --git a/app/src/main/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml index 75ec8af2..21c49844 100644 --- a/app/src/main/res/menu/mediaplayer.xml +++ b/app/src/main/res/menu/mediaplayer.xml @@ -13,7 +13,7 @@ diff --git a/app/src/main/res/menu/subscriptions.xml b/app/src/main/res/menu/subscriptions.xml index ec991e42..fb3309ab 100644 --- a/app/src/main/res/menu/subscriptions.xml +++ b/app/src/main/res/menu/subscriptions.xml @@ -22,9 +22,4 @@ android:title="@string/refresh_label" android:menuCategory="container" custom:showAsAction="never" /> - diff --git a/changelog.md b/changelog.md index 3c051cf8..0d69c693 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,16 @@ +## 6.0.4 + +* bug fix on ShareDialog having no argument +* tuned and fixed menu issues in EpisodeIndo and PlayerDetailed views +* corrected current order in FeedEpisode sort dialog +* fixed sorting in Subscriptions view +* fixed tags spinner update issue in Subscriptions view +* made various episodes list views to reflect change on status changes of episodes +* fixed DB write error when deleting a feed +* fixed illegal index error in AllEpisodes and History views when the list is empty +* synchronized feeds list update when adding or deleting multiple feeds +* added "Remove from favorites" in speed-dial menu + ## 6.0.3 * minor class restructuring diff --git a/fastlane/metadata/android/en-US/changelogs/3020204.txt b/fastlane/metadata/android/en-US/changelogs/3020204.txt new file mode 100644 index 00000000..911896f3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020204.txt @@ -0,0 +1,13 @@ + +Version 6.0.4 brings several changes: + +* bug fix on ShareDialog having no argument +* tuned and fixed menu issues in EpisodeIndo and PlayerDetailed views +* corrected current order in FeedEpisode sort dialog +* fixed sorting in Subscriptions view +* fixed tags spinner update issue in Subscriptions view +* made various episodes list views to reflect change on status changes of episodes +* fixed DB write error when deleting a feed +* fixed illegal index error in AllEpisodes and History views when the list is empty +* synchronized feeds list update when adding or deleting multiple feeds +* added "Remove from favorites" in speed-dial menu