6.0.4 commit

This commit is contained in:
Xilin Jia 2024-06-30 20:50:02 +01:00
parent 8d8db46adc
commit a21e99fa08
27 changed files with 326 additions and 280 deletions

View File

@ -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 = ""

View File

@ -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) {

View File

@ -45,7 +45,7 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
findPreference<Preference>(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
}

View File

@ -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))
}
}
}

View File

@ -36,6 +36,7 @@ object Feeds {
private val feedMap: MutableMap<Long, Feed> = mutableMapOf()
private val tags: MutableList<String> = mutableListOf()
@Synchronized
fun getFeedList(): List<Feed> {
return feedMap.values.toList()
}
@ -44,6 +45,7 @@ object Feeds {
return tags
}
@Synchronized
fun updateFeedMap(feeds: List<Feed> = 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<String>()
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<Episode>) {
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<Episode>) {
// 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 {

View File

@ -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
}
}

View File

@ -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

View File

@ -23,7 +23,8 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
fun handleAction(items: List<Episode>) {
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<Episode>) {
private fun markFavorite(items: List<Episode>, stat: Boolean) {
for (item in items) {
Episodes.setFavorite(item, true)
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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"

View File

@ -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))
}
}
}

View File

@ -62,6 +62,7 @@ import kotlin.math.min
override fun loadData(): List<Episode> {
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))
}

View File

@ -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<Chapter> = media.getChapters()
if (currentMedia!!.getChapters().isNotEmpty()) {
val chapters: List<Chapter> = 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()

View File

@ -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 -> {}

View File

@ -59,7 +59,7 @@ import java.util.*
private val binding get() = _binding!!
private var runningDownloads: Set<String> = HashSet()
private var items: MutableList<Episode> = mutableListOf()
private var episodes: MutableList<Episode> = 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<String> = 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

View File

@ -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
}
}

View File

@ -183,8 +183,7 @@ import java.util.concurrent.Semaphore
}
})
dialBinding.fabSD.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id)
.handleAction(adapter.selectedItems.filterIsInstance<Episode>())
EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(adapter.selectedItems.filterIsInstance<Episode>())
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

View File

@ -152,6 +152,7 @@ import kotlin.math.min
override fun loadData(): List<Episode> {
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))
}

View File

@ -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)
}

View File

@ -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<String>
private var tagFilterIndex = 1
// TODO: currently not used
private var displayedFolder: String = ""
private var displayUpArrow = false
private var feedList: MutableList<Feed> = mutableListOf()
private var feedListFiltered: List<Feed> = mutableListOf()
private val tags: MutableList<String> = 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<Feed> = 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<Episode>): Map<Long, Long> {
@ -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<Feed> = ArrayList<Feed>()
for (item in feedListFiltered) {
feedsOnly.add(item)
}
val feedsOnly: MutableList<Feed> = ArrayList<Feed>(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<T : RecyclerView.ViewHolder?> : SelectableAdapter<T>(activity as MainActivity), View.OnCreateContextMenuListener {
protected var feedList: List<Feed>
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<String> = 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
}
}
}

View File

@ -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

View File

@ -42,4 +42,9 @@
android:icon="@drawable/ic_star"
android:title="@string/add_to_favorite_label" />
<item
android:id="@+id/remove_favorite_batch"
android:icon="@drawable/ic_star_border"
android:title="@string/remove_from_favorite_label" />
</menu>

View File

@ -13,7 +13,7 @@
<item
android:id="@+id/show_video"
android:icon="@drawable/baseline_fullscreen_24"
android:title="@string/add_to_favorite_label"
android:title="@string/show_video_label"
android:visible="false"
custom:showAsAction="always">
</item>

View File

@ -22,9 +22,4 @@
android:title="@string/refresh_label"
android:menuCategory="container"
custom:showAsAction="never" />
<item
android:id="@+id/subscriptions_filter"
android:title="@string/filter"
android:visible="false"
custom:showAsAction="never"/>
</menu>

View File

@ -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

View File

@ -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