diff --git a/Licenses_and_permissions.md b/Licenses_and_permissions.md index d9f757d0..9492bb91 100644 --- a/Licenses_and_permissions.md +++ b/Licenses_and_permissions.md @@ -24,8 +24,6 @@ Apache License 2.0 [com.squareup.okhttp3](https://github.com/square/okhttp/blob/master/LICENSE.txt) Apache License 2.0 -[//]: # ([io.reactivex.rxjava2](https://github.com/ReactiveX/RxJava/blob/3.x/LICENSE) Apache License 2.0) - [com.mikepenz:iconics-core](https://github.com/mikepenz/Android-Iconics/blob/develop/LICENSE) Apache License 2.0 [com.leinardi.android](https://github.com/leinardi/FloatingActionButtonSpeedDial/blob/release/LICENSE) Apache License 2.0 diff --git a/README.md b/README.md index 769d1d12..c62c5db7 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * More convenient player control displayed on all pages * Revamped and more efficient expanded player view showing episode description on the front -* External player class is merged into the player * Playback speed setting has been straightened up, three speed can be set separately or combined: current audio, podcast, and global * Added preference "Fast Forward Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a number between 0.0 and 10.0 * The "Skip to next episode" button on the player @@ -50,9 +49,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * single tap not during play has no effect * Added preference "Fallback Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 1.5) * if the user customizes "Fallback speed" to a value greater than 0.1, long-press the Play button during play enters the fallback mode and plays at the set fallback speed, single tap exits the fallback mode -* Various efficiency improvements, including removal of: - * redundant media loadings and ui updates - * frequent list search during audio play +* Various efficiency improvements * streamed media somewhat equivalent to downloaded media * enabled episode description on player detailed view * enabled intro- and end- skipping @@ -77,6 +74,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * Played or new episodes have clearer markings * Sort dialog no longer dims the main view * download date can be used to sort both feeds and episodes +* Subscriptions view has a filter based on feed preferences, in the same style as episodes filter * Subscriptions sorting is now bi-directional based on various explicit measures * in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward) * Long-press filter button in FeedEpisode view enables/disables filters without changing filter settings @@ -106,8 +104,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * Ability to open podcast from webpage address * Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes * Online feed episodes can be freely played (streamed) without a subscription -* externally shared feed opens in the new online feed view fragment -* OnlineFeedView` activity is stripped down to only receive externally shared feeds * Youtube channels are accepted from external share or paste of address in podcast search view, and can be subscribed as a normal podcast, though video play is handled externally ### Instant (or Wifi) sync @@ -122,6 +118,8 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * When auto download is enabled in the Settings, feeds to be auto-downloaded need to be separately enabled in the feed settings. * Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. "newest episodes" meaning most recent episodes, new or old) * Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app. + * Auto downloads run feeds or feed refreshes, scheduled or manual + * auto download always includes any undownloaded episodes (regardless of feeds) added in the current queue * After auto download run, episodes with New status is changed to Unplayed. * auto download feed setting dialog is also changed: * there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently diff --git a/app/build.gradle b/app/build.gradle index 7a7f9e02..b4f6c8be 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -126,8 +126,8 @@ android { buildConfig true } defaultConfig { - versionCode 3020215 - versionName "6.1.1" + versionCode 3020216 + versionName "6.1.2" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 2b9f8fb8..98dd6ca1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -7,6 +7,7 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink +import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileEpisodeDownload import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs @@ -83,7 +84,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result workInfoList.forEach { workInfo -> if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) { - if (media.episode != null) Queues.removeFromQueue(null, media.episode!!) + if (media.episode != null) Queues.removeFromQueue(media.episode!!) } } WorkManager.getInstance(context).cancelAllWorkByTag(tag) @@ -383,7 +384,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message) updatedStatus = DownloadResult(media.id, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"") } - if (item != null) { + if (needSynch() && item != null) { val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD) .currentTimestamp() .build() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt index fea8357b..01cdbf1d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt @@ -356,15 +356,9 @@ object FeedUpdateManager { private const val serialVersionUID = 1L } } - - companion object { - private val TAG: String = FeedParserTask::class.simpleName ?: "Anonymous" - } } class FeedSyncTask(private val context: Context, request: DownloadRequest) { -// var savedFeed: Feed? = null -// private set private val task = FeedParserTask(request) private var feedHandlerResult: FeedHandlerResult? = null val downloadStatus: DownloadResult @@ -379,9 +373,5 @@ object FeedUpdateManager { return true } } - - companion object { - private val TAG: String = FeedUpdateWorker::class.simpleName ?: "Anonymous" - } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt index 9914b784..24fdf45a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt @@ -287,7 +287,7 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont // if (result.first != null) queueToBeRemoved.add(result.second) updatedItems.add(result.second) } - removeFromQueue(null, *updatedItems.toTypedArray()) + removeFromQueue(*updatedItems.toTypedArray()) // loadAdditionalFeedItemListData(updatedItems) persistEpisodes(updatedItems) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt index 39f95951..d712fa23 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt @@ -11,6 +11,10 @@ object SynchronizationQueueSink { // To avoid a dependency loop of every class to SyncService, and from SyncService back to every class. private var serviceStarterImpl = Runnable {} + fun needSynch() : Boolean { + return isProviderConnected + } + fun setServiceStarterImpl(serviceStarter: Runnable) { serviceStarterImpl = serviceStarter } @@ -57,9 +61,9 @@ object SynchronizationQueueSink { fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) { if (!isProviderConnected) return + if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return - val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY) .currentTimestamp() .started(media.startPosition / 1000) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt index f31a3f30..d931fb76 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt @@ -3,6 +3,7 @@ package ac.mdiq.podcini.playback.base import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia import ac.mdiq.podcini.storage.database.RealmDB.realm +import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.* @@ -45,7 +46,7 @@ object InTheatre { Logd(TAG, "starting curQueue") var curQueue_ = realm.query(PlayQueue::class).sort("updated", Sort.DESCENDING).first().find() if (curQueue_ != null) { - curQueue = realm.copyFromRealm(curQueue_) + curQueue = unmanaged(curQueue_) curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) .find().sortedBy { curQueue.episodeIds.indexOf(it.id) })) } @@ -68,7 +69,7 @@ object InTheatre { Logd(TAG, "starting curState") var curState_ = realm.query(CurrentState::class).first().find() - if (curState_ != null) curState = realm.copyFromRealm(curState_) + if (curState_ != null) curState = unmanaged(curState_) else { Logd(TAG, "creating new curState") curState_ = CurrentState() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index ca7b5f8b..c6a6b4e5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -312,7 +312,7 @@ class PlaybackService : MediaSessionService() { (action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!))) if (playable is EpisodeMedia && shouldAutoDelete && (item?.isFavorite != true || !shouldFavoriteKeepEpisode())) { item = deleteMediaSync(this@PlaybackService, item!!) - if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, null, item!!) + if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item!!) } } if (playable is EpisodeMedia && (ended || skipped || playingNext)) addToHistory(item!!) 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 2393b310..fe3a0c11 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -43,6 +43,8 @@ object UserPreferences { prefDefaultPage, prefBackButtonOpensDrawer, + prefFeedFilter, + prefQueueKeepSorted, prefQueueKeepSortedOrder, prefDownloadSortedOrder, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt index 9dcf74af..05322189 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt @@ -23,8 +23,9 @@ import java.util.* import java.util.concurrent.ExecutionException object AutoCleanups { + private val TAG: String = AutoCleanups::class.simpleName ?: "Anonymous" - var episodeCleanupValue: Int + private var episodeCleanupValue: Int get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodeCleanup.name, "" + EPISODE_CLEANUP_NULL)!!.toInt() set(episodeCleanupValue) { appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply() @@ -102,9 +103,6 @@ object AutoCleanups { } return 0 } - companion object { - private val TAG: String = ExceptFavoriteCleanupAlgorithm::class.simpleName ?: "Anonymous" - } } /** @@ -154,9 +152,6 @@ object AutoCleanups { public override fun getDefaultCleanupParameter(): Int { return getNumEpisodesToCleanup(0) } - companion object { - private val TAG: String = APQueueCleanupAlgorithm::class.simpleName ?: "Anonymous" - } } /** @@ -174,9 +169,6 @@ object AutoCleanups { override fun getReclaimableItems(): Int { return 0 } - companion object { - private val TAG: String = APNullCleanupAlgorithm::class.simpleName ?: "Anonymous" - } } /** the number of days after playback to wait before an item is eligible to be cleaned up. @@ -232,7 +224,6 @@ object AutoCleanups { return getNumEpisodesToCleanup(0) } companion object { - private val TAG: String = APCleanupAlgorithm::class.simpleName ?: "Anonymous" private fun minusHours(baseDate: Date, numberOfHours: Int): Date { val cal = Calendar.getInstance() cal.time = baseDate diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt index 74155f10..24cf7254 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt @@ -127,9 +127,6 @@ object AutoDownloads { val status = batteryStatus!!.getIntExtra(BatteryManager.EXTRA_STATUS, -1) return (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL) } - companion object { - private val TAG: String = AutoDownloadAlgorithm::class.simpleName ?: "Anonymous" - } } class FeedBasedAutoDLAlgorithm : AutoDownloadAlgorithm() { @@ -141,10 +138,10 @@ object AutoDownloads { val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload) // true if we should auto download based on power status val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery) - Logd(Companion.TAG, "autoDownloadEpisodeMedia prepare $networkShouldAutoDl $powerShouldAutoDl") + Logd(TAG, "autoDownloadEpisodeMedia prepare $networkShouldAutoDl $powerShouldAutoDl") // we should only auto download if both network AND power are happy if (networkShouldAutoDl && powerShouldAutoDl) { - Logd(Companion.TAG, "autoDownloadEpisodeMedia Performing auto-dl of undownloaded episodes") + Logd(TAG, "autoDownloadEpisodeMedia Performing auto-dl of undownloaded episodes") val candidates: MutableSet = mutableSetOf() val queueItems = realm.query(Episode::class).query("id IN $0 AND media.downloaded == false", curQueue.episodeIds).find() Logd(TAG, "autoDownloadEpisodeMedia add from queue: ${queueItems.size}") @@ -155,6 +152,7 @@ object AutoDownloads { var episodes = mutableListOf() val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name), f.id) val allowedDLCount = (f.preferences?.autoDLMaxEpisodes?:0) - downloadedCount + Logd(TAG, "autoDownloadEpisodeMedia ${f.preferences?.autoDLMaxEpisodes} downloadedCount: $downloadedCount allowedDLCount: $allowedDLCount") if (allowedDLCount > 0) { var queryString = "feedId == ${f.id} AND isAutoDownloadEnabled == true AND media != nil AND media.downloaded == false" when (f.preferences?.autoDLPolicy) { @@ -225,8 +223,5 @@ object AutoDownloads { else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl") } } - companion object { - private val TAG: String = FeedBasedAutoDLAlgorithm::class.simpleName ?: "Anonymous" - } } } \ No newline at end of file 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 881efb40..e5c04e4d 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 @@ -5,6 +5,7 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink +import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying @@ -99,7 +100,7 @@ object Episodes { return runOnIOScope { if (episode.media == null) return@runOnIOScope val episode_ = deleteMediaSync(context, episode) - if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, null, episode_) + if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, episode_) } } @@ -158,9 +159,11 @@ object Episodes { // Do full update of this feed to get rid of the episode if (episode.feed != null) updateFeed(episode.feed!!, context.applicationContext, null) } else { - // Gpodder: queue delete action for synchronization - val action = EpisodeAction.Builder(episode, EpisodeAction.DELETE).currentTimestamp().build() - SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) + if (needSynch()) { + // Gpodder: queue delete action for synchronization + val action = EpisodeAction.Builder(episode, EpisodeAction.DELETE).currentTimestamp().build() + SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) + } EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(episode)) } return episode @@ -292,6 +295,6 @@ object Episodes { } private fun shouldRemoveFromQueuesMarkPlayed(): Boolean { - return appPrefs.getBoolean(UserPreferences.Prefs.prefRemoveFromQueueMarkedPlayed.name, true) + return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true) } } \ No newline at end of file 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 952e62a3..22b6cae3 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 @@ -4,6 +4,7 @@ import ac.mdiq.podcini.BuildConfig import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink +import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes @@ -39,8 +40,11 @@ object Feeds { private val tags: MutableList = mutableListOf() @Synchronized - fun getFeedList(fromDB: Boolean = true): List { - if (fromDB) return realm.query(Feed::class).find() + fun getFeedList(queryString: String = "", fromDB: Boolean = true): List { + if (fromDB) { + return if (queryString.isEmpty()) realm.query(Feed::class).find() + else realm.query(Feed::class, queryString).find() + } return feedMap.values.toList() } @@ -278,7 +282,7 @@ object Feeds { """.trimIndent())) oldItem.identifier = episode.identifier - if (oldItem.isPlayed() && oldItem.media != null) { + if (needSynch() && oldItem.isPlayed() && oldItem.media != null) { val durs = oldItem.media!!.getDuration() / 1000 val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY) .currentTimestamp() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index 611bad9c..1a7fc2ab 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -15,7 +15,6 @@ import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import android.content.Context import android.util.Log import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi @@ -29,6 +28,60 @@ object Queues { BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM } + var isQueueLocked: Boolean + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, false) + set(locked) { + appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply() + } + + var isQueueKeepSorted: Boolean + /** + * Returns if the queue is in keep sorted mode. + * @see .queueKeepSortedOrder + */ + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, false) + /** + * Enables/disables the keep sorted mode of the queue. + * @see .queueKeepSortedOrder + */ + set(keepSorted) { + appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, keepSorted).apply() + } + + var queueKeepSortedOrder: EpisodeSortOrder? + /** + * Returns the sort order for the queue keep sorted mode. + * Note: This value is stored independently from the keep sorted state. + * @see .isQueueKeepSorted + */ + get() { + val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, "use-default") + return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD) + } + /** + * Sets the sort order for the queue keep sorted mode. + * @see .setQueueKeepSorted + */ + set(sortOrder) { + if (sortOrder == null) return + appPrefs.edit().putString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, sortOrder.name).apply() + } + + var enqueueLocation: EnqueueLocation + get() { + val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name) + try { + return EnqueueLocation.valueOf(valStr!!) + } catch (t: Throwable) { + // should never happen but just in case + Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t) + return EnqueueLocation.BACK + } + } + set(location) { + appPrefs.edit().putString(UserPreferences.Prefs.prefEnqueueLocation.name, location.name).apply() + } + fun getInQueueEpisodeIds(): Set { Logd(TAG, "getQueueIDList() called") val queues = realm.query(PlayQueue::class).find() @@ -89,24 +142,25 @@ object Queues { } } - suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode) { + suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode, queue_: PlayQueue? = null) { Logd(TAG, "addToQueueSync( ... ) called") + val queue = queue_ ?: curQueue val currentlyPlaying = curMedia val positionCalculator = EnqueuePositionCalculator(enqueueLocation) - var insertPosition = positionCalculator.calcPosition(curQueue.episodes, currentlyPlaying) + var insertPosition = positionCalculator.calcPosition(queue.episodes, currentlyPlaying) - if (curQueue.episodeIds.contains(episode.id)) return + if (queue.episodeIds.contains(episode.id)) return - curQueue.episodeIds.add(insertPosition, episode.id) - curQueue.episodes.add(insertPosition, episode) + queue.episodeIds.add(insertPosition, episode.id) + queue.episodes.add(insertPosition, episode) insertPosition++ - curQueue.update() - upsert(curQueue) {} + queue.update() + upsert(queue) {} if (markAsUnplayed && episode.isNew) setPlayState(Episode.UNPLAYED, false, episode) - EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition)) + if (queue_?.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition)) // if (performAutoDownload) autodownloadEpisodeMedia(context) } @@ -147,13 +201,11 @@ object Queues { /** * Removes a Episode object from the queue. - * @param context A context that is used for opening a database connection. - * perform autodownloadEpisodeMedia only if context is not null * @param episodes FeedItems that should be removed. */ @OptIn(UnstableApi::class) @JvmStatic - fun removeFromQueue(context: Context?, vararg episodes: Episode) : Job { - return runOnIOScope { removeFromQueueSync(curQueue, context, *episodes) } + fun removeFromQueue(vararg episodes: Episode) : Job { + return runOnIOScope { removeFromQueueSync(curQueue, *episodes) } } @OptIn(UnstableApi::class) @@ -161,22 +213,21 @@ object Queues { Logd(TAG, "removeFromAllQueues called ") val queues = realm.query(PlayQueue::class).find() for (q in queues) { - if (q.id != curQueue.id) removeFromQueueSync(q, null, *episodes) + if (q.id != curQueue.id) removeFromQueueSync(q, *episodes) } // ensure curQueue is last updated - removeFromQueueSync(curQueue, null, *episodes) + removeFromQueueSync(curQueue, *episodes) } /** * @param queue_ if null, use curQueue - * @param context perform autodownloadEpisodeMedia only if context is not null and queue_ is curQueue */ @UnstableApi - internal fun removeFromQueueSync(queue_: PlayQueue?, context: Context?, vararg episodes: Episode) { + internal fun removeFromQueueSync(queue_: PlayQueue?, vararg episodes: Episode) { Logd(TAG, "removeFromQueueSync called ") if (episodes.isEmpty()) return - val queue = queue_ ?: curQueue + var queue = queue_ ?: curQueue val events: MutableList = ArrayList() val pos: MutableList = mutableListOf() val qItems = queue.episodes.toMutableList() @@ -200,9 +251,6 @@ object Queues { } for (event in events) EventFlow.postEvent(event) } else Logd(TAG, "Queue was not modified by call to removeQueueItem") - -// TODO: what's this for? -// if (queue.id == curQueue.id && context != null) autodownloadEpisodeMedia(context) } suspend fun removeFromAllQueuesQuiet(episodeIds: List) { @@ -214,10 +262,11 @@ object Queues { eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() if (eidsInQueues.isNotEmpty()) { val qeids = q.episodeIds.minus(eidsInQueues) - q.episodeIds.clear() - q.episodeIds.addAll(qeids) - q.update() - upsert(q) {} + upsert(q) { + it.episodeIds.clear() + it.episodeIds.addAll(qeids) + it.update() + } } } // ensure curQueue is last updated @@ -225,10 +274,11 @@ object Queues { eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() if (eidsInQueues.isNotEmpty()) { val qeids = q.episodeIds.minus(eidsInQueues) - q.episodeIds.clear() - q.episodeIds.addAll(qeids) - q.update() - upsert(q) {} + upsert(q) { + it.episodeIds.clear() + it.episodeIds.addAll(qeids) + it.update() + } } } @@ -270,60 +320,6 @@ object Queues { upsertBlk(curQueue) {} } - var isQueueLocked: Boolean - get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, false) - set(locked) { - appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply() - } - - var isQueueKeepSorted: Boolean - /** - * Returns if the queue is in keep sorted mode. - * @see .queueKeepSortedOrder - */ - get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, false) - /** - * Enables/disables the keep sorted mode of the queue. - * @see .queueKeepSortedOrder - */ - set(keepSorted) { - appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, keepSorted).apply() - } - - var queueKeepSortedOrder: EpisodeSortOrder? - /** - * Returns the sort order for the queue keep sorted mode. - * Note: This value is stored independently from the keep sorted state. - * @see .isQueueKeepSorted - */ - get() { - val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, "use-default") - return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD) - } - /** - * Sets the sort order for the queue keep sorted mode. - * @see .setQueueKeepSorted - */ - set(sortOrder) { - if (sortOrder == null) return - appPrefs.edit().putString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, sortOrder.name).apply() - } - - var enqueueLocation: EnqueueLocation - get() { - val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name) - try { - return EnqueueLocation.valueOf(valStr!!) - } catch (t: Throwable) { - // should never happen but just in case - Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t) - return EnqueueLocation.BACK - } - } - set(location) { - appPrefs.edit().putString(UserPreferences.Prefs.prefEnqueueLocation.name, location.name).apply() - } - class EnqueuePositionCalculator(private val enqueueLocation: EnqueueLocation) { /** * Determine the position (0-based) that the item(s) should be inserted to the named queue. diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt index 32317bec..65ac0631 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt @@ -4,38 +4,22 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue import java.io.Serializable class EpisodeFilter(vararg properties: String) : Serializable { - private val properties: Array = arrayOf(*properties.filter { it.isNotEmpty() }.map {it.trim()}.toTypedArray()) - @JvmField val showPlayed: Boolean = hasProperty(States.played.name) - @JvmField val showUnplayed: Boolean = hasProperty(States.unplayed.name) - @JvmField val showPaused: Boolean = hasProperty(States.paused.name) - @JvmField val showNotPaused: Boolean = hasProperty(States.not_paused.name) - @JvmField val showNew: Boolean = hasProperty(States.new.name) - @JvmField val showQueued: Boolean = hasProperty(States.queued.name) - @JvmField val showNotQueued: Boolean = hasProperty(States.not_queued.name) - @JvmField val showDownloaded: Boolean = hasProperty(States.downloaded.name) - @JvmField val showNotDownloaded: Boolean = hasProperty(States.not_downloaded.name) - @JvmField val showAutoDownloadable: Boolean = hasProperty(States.auto_downloadable.name) - @JvmField val showNotAutoDownloadable: Boolean = hasProperty(States.not_auto_downloadable.name) - @JvmField val showHasMedia: Boolean = hasProperty(States.has_media.name) - @JvmField val showNoMedia: Boolean = hasProperty(States.no_media.name) - @JvmField val showIsFavorite: Boolean = hasProperty(States.is_favorite.name) - @JvmField val showNotFavorite: Boolean = hasProperty(States.not_favorite.name) constructor(properties: String) : this(*(properties.split(",").toTypedArray())) @@ -115,6 +99,7 @@ class EpisodeFilter(vararg properties: String) : Serializable { return query.toString() } + @Suppress("EnumEntryName") enum class States { played, unplayed, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt index 19a4315f..73e90cd0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt @@ -99,11 +99,11 @@ class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilt } fun hasIncludeFilter(): Boolean { - return includeFilterRaw!!.isNotEmpty() + return !includeFilterRaw.isNullOrEmpty() } fun hasExcludeFilter(): Boolean { - return excludeFilterRaw!!.isNotEmpty() + return !excludeFilterRaw.isNullOrEmpty() } fun hasMinimalDurationFilter(): Boolean { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedFilter.kt new file mode 100644 index 00000000..7c9b8744 --- /dev/null +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedFilter.kt @@ -0,0 +1,105 @@ +package ac.mdiq.podcini.storage.model + +import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.SPEED_USE_GLOBAL +import java.io.Serializable + +class FeedFilter(vararg properties: String) : Serializable { + + private val properties: Array = arrayOf(*properties.filter { it.isNotEmpty() }.map {it.trim()}.toTypedArray()) + + val showKeepUpdated: Boolean = hasProperty(States.keepUpdated.name) + val showNotKeepUpdated: Boolean = hasProperty(States.not_keepUpdated.name) + val showGlobalPlaySpeed: Boolean = hasProperty(States.global_playSpeed.name) + val showCustomPlaySpeed: Boolean = hasProperty(States.custom_playSpeed.name) + val showHasSkips: Boolean = hasProperty(States.has_skips.name) + val showNoSkips: Boolean = hasProperty(States.no_skips.name) + val showAlwaysAutoDelete: Boolean = hasProperty(States.always_auto_delete.name) + val showNeverAutoDelete: Boolean = hasProperty(States.never_auto_delete.name) + val showAutoDownload: Boolean = hasProperty(States.autoDownload.name) + val showNotAutoDownload: Boolean = hasProperty(States.not_autoDownload.name) + + constructor(properties: String) : this(*(properties.split(",").toTypedArray())) + + private fun hasProperty(property: String): Boolean { + return listOf(*properties).contains(property) + } + + val values: Array + get() = properties.clone() + + val valuesList: List + get() = listOf(*properties) + + fun matches(feed: Feed): Boolean { + when { + showKeepUpdated && feed.preferences?.keepUpdated != true -> return false + showNotKeepUpdated && feed.preferences?.keepUpdated != false -> return false + showGlobalPlaySpeed && feed.preferences?.playSpeed != SPEED_USE_GLOBAL -> return false + showCustomPlaySpeed && feed.preferences?.playSpeed == SPEED_USE_GLOBAL -> return false + showHasSkips && feed.preferences?.introSkip == 0 && feed.preferences?.endingSkip == 0 -> return false + showNoSkips && (feed.preferences?.introSkip != 0 || feed.preferences?.endingSkip != 0) -> return false + showAlwaysAutoDelete && feed.preferences?.autoDeleteAction != FeedPreferences.AutoDeleteAction.ALWAYS -> return false + showNeverAutoDelete && feed.preferences?.autoDeleteAction != FeedPreferences.AutoDeleteAction.NEVER -> return false + showAutoDownload && feed.preferences?.autoDownload != true -> return false + showNotAutoDownload && feed.preferences?.autoDownload != false -> return false + else -> return true + } + } + + fun queryString(): String { + val statements: MutableList = ArrayList() + when { + showKeepUpdated -> statements.add("preferences.keepUpdated == true ") + showNotKeepUpdated -> statements.add(" preferences.keepUpdated == false ") + } + when { + showGlobalPlaySpeed -> statements.add(" preferences.playSpeed == ${SPEED_USE_GLOBAL} ") + showCustomPlaySpeed -> statements.add(" preferences.playSpeed != $SPEED_USE_GLOBAL ") + } + when { + showHasSkips -> statements.add(" preferences.introSkip != 0 OR preferences.endingSkip != 0 ") + showNoSkips -> statements.add(" preferences.introSkip == 0 AND preferences.endingSkip == 0 ") + } + when { + showAlwaysAutoDelete -> statements.add(" preferences.autoDelete == ${FeedPreferences.AutoDeleteAction.ALWAYS.code} ") + showNeverAutoDelete -> statements.add(" preferences.playSpeed != ${FeedPreferences.AutoDeleteAction.NEVER.code} ") + } + when { + showAutoDownload -> statements.add(" preferences.autoDownload == true ") + showNotAutoDownload -> statements.add(" preferences.autoDownload == false ") + } + + if (statements.isEmpty()) return "id > 0" + + val query = StringBuilder(" (" + statements[0]) + for (r in statements.subList(1, statements.size)) { + query.append(" AND ") + query.append(r) + } + query.append(") ") + + return query.toString() + } + + @Suppress("EnumEntryName") + enum class States { + keepUpdated, + not_keepUpdated, + global_playSpeed, + custom_playSpeed, + has_skips, + no_skips, +// global_auto_delete, + always_auto_delete, + never_auto_delete, + autoDownload, + not_autoDownload, + + } + companion object { + @JvmStatic + fun unfiltered(): FeedFilter { + return FeedFilter("") + } + } +} 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 4d828786..d8512a5a 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 @@ -1,19 +1,36 @@ package ac.mdiq.podcini.ui.actions -import ac.mdiq.podcini.ui.activity.MainActivity -import android.util.Log -import androidx.annotation.PluralsRes -import com.google.android.material.snackbar.Snackbar import ac.mdiq.podcini.R -import ac.mdiq.podcini.storage.model.Episode +import ac.mdiq.podcini.databinding.SwitchQueueDialogBinding import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface +import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.storage.database.Episodes -import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Queues +import ac.mdiq.podcini.storage.database.Queues.addToQueueSync +import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet import ac.mdiq.podcini.storage.database.Queues.removeFromQueue +import ac.mdiq.podcini.storage.database.RealmDB.realm +import ac.mdiq.podcini.storage.database.RealmDB.unmanaged +import ac.mdiq.podcini.storage.model.Episode +import ac.mdiq.podcini.storage.model.PlayQueue +import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.utils.LocalDeleteModal +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import android.app.Activity +import android.content.DialogInterface +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.annotation.PluralsRes import androidx.media3.common.util.UnstableApi +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.runBlocking +import java.lang.ref.WeakReference @UnstableApi @@ -26,6 +43,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val R.id.add_to_favorite_batch -> markFavorite(items, true) R.id.remove_favorite_batch -> markFavorite(items, false) R.id.add_to_queue_batch -> queueChecked(items) + R.id.put_to_queue_batch -> putToQueue(items) R.id.remove_from_queue_batch -> removeFromQueueChecked(items) R.id.mark_read_batch -> { setPlayState(Episode.PLAYED, false, *items.toTypedArray()) @@ -53,7 +71,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val private fun removeFromQueueChecked(items: List) { val checkedIds = getSelectedIds(items) - removeFromQueue(activity, *items.toTypedArray()) + removeFromQueue(*items.toTypedArray()) showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.size) } @@ -96,6 +114,59 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val return checkedIds } + private fun putToQueue(items: List) { + PutToQueueDialog(activity as MainActivity, items).show() + } + + class PutToQueueDialog(activity: Activity, val items: List) { + private val activityRef: WeakReference = WeakReference(activity) + + fun show() { + val activity = activityRef.get() ?: return + val binding = SwitchQueueDialogBinding.inflate(LayoutInflater.from(activity)) + val queues = realm.query(PlayQueue::class).find() + val queueNames = queues.map { it.name }.toTypedArray() + val adaptor = ArrayAdapter(activity, android.R.layout.simple_spinner_item, queueNames) + adaptor.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + val catSpinner = binding.queueSpinner + catSpinner.setAdapter(adaptor) + catSpinner.setSelection(adaptor.getPosition(curQueue.name)) + var toQueue: PlayQueue = curQueue + catSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + toQueue = unmanaged(queues[position]) + } + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + MaterialAlertDialogBuilder(activity) + .setView(binding.root) + .setTitle(R.string.switch_queue_label) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + val queues = realm.query(PlayQueue::class).find() + val toRemove = mutableSetOf() + val toRemoveCur = mutableListOf() + items.forEach { e -> + if (curQueue.isInQueue(e)) toRemoveCur.add(e) + } + items.forEach { e -> + for (q in queues) { + if (q.isInQueue(e)) { + toRemove.add(e.id) + break + } + } + } + if (toRemove.isNotEmpty()) runBlocking { removeFromAllQueuesQuiet(toRemove.toList()) } + if (toRemoveCur.isNotEmpty()) EventFlow.postEvent(FlowEvent.QueueEvent.removed(toRemoveCur)) + items.forEach { e -> + runBlocking { addToQueueSync(false, e, toQueue) } + } + } + .setNegativeButton(R.string.cancel_label, null) + .show() + } + } + companion object { private val TAG: String = EpisodeMultiSelectHandler::class.simpleName ?: "Anonymous" } 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 69e3b0bd..ca404dda 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 @@ -5,15 +5,15 @@ import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink +import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE import ac.mdiq.podcini.receiver.MediaButtonReceiver -import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode -import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Episodes.setFavorite +import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Queues.addToQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueue import ac.mdiq.podcini.storage.model.Episode @@ -145,7 +145,7 @@ object EpisodeMenuHandler { if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) { val media: EpisodeMedia? = selectedItem.media // not all items have media, Gpodder only cares about those that do - if (media != null) { + if (needSynch() && media != null) { val actionPlay: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY) .currentTimestamp() .started(media.getDuration() / 1000) @@ -159,7 +159,7 @@ object EpisodeMenuHandler { R.id.mark_unread_item -> { selectedItem.setPlayed(false) setPlayState(Episode.UNPLAYED, false, selectedItem) - if (selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) { + if (needSynch() && selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) { val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW) .currentTimestamp() .build() @@ -167,7 +167,7 @@ object EpisodeMenuHandler { } } R.id.add_to_queue_item -> addToQueue(true, selectedItem) - R.id.remove_from_queue_item -> removeFromQueue(context, selectedItem) + R.id.remove_from_queue_item -> removeFromQueue(selectedItem) R.id.add_to_favorites_item -> setFavorite(selectedItem, true) R.id.remove_from_favorites_item -> setFavorite(selectedItem, false) R.id.reset_position -> { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/RemoveFromQueueSwipeAction.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/RemoveFromQueueSwipeAction.kt index 201d0d84..98824631 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/RemoveFromQueueSwipeAction.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/RemoveFromQueueSwipeAction.kt @@ -37,7 +37,7 @@ class RemoveFromQueueSwipeAction : SwipeAction { @OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) { val position: Int = curQueue.episodes.indexOf(item) - removeFromQueue(fragment.requireActivity(), item) + removeFromQueue(item) if (willRemove(filter, item)) { (fragment.requireActivity() as MainActivity).showSnackbarAbovePlayer(fragment.resources.getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), Snackbar.LENGTH_LONG) .setAction(fragment.getString(R.string.undo)) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt index 8780852c..6883e879 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt @@ -52,7 +52,7 @@ class TogglePlaybackStateSwipeAction : SwipeAction { val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!) if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) { item = deleteMediaSync(fragment.requireContext(), item) - if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, null, item) } + if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) } } val playStateStringRes: Int = when (newState) { Episode.UNPLAYED -> if (item.playState == Episode.NEW) R.string.removed_inbox_label //was new @@ -83,7 +83,7 @@ class TogglePlaybackStateSwipeAction : SwipeAction { if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) { // deleteMediaOfEpisode(fragment.requireContext(), item) var item = deleteMediaSync(fragment.requireContext(), item) - if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, null, item) } + if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) } } override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt index 47e6863a..98f23f6d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt @@ -45,7 +45,7 @@ abstract class EpisodeFilterDialog : BottomSheetDialogFragment() { val layout = inflater.inflate(R.layout.filter_dialog, null, false) _binding = FilterDialogBinding.bind(layout) rows = binding.filterRows - Logd("ItemFilterDialog", "fragment onCreateView") + Logd("EpisodeFilterDialog", "fragment onCreateView") //add filter rows for (item in FeedItemFilterGroup.entries) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedFilterDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedFilterDialog.kt new file mode 100644 index 00000000..d0b6dcc9 --- /dev/null +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedFilterDialog.kt @@ -0,0 +1,147 @@ +package ac.mdiq.podcini.ui.dialog + +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.FilterDialogBinding +import ac.mdiq.podcini.databinding.FilterDialogRowBinding +import ac.mdiq.podcini.storage.model.FeedFilter +import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.TAG +import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedsFilter +import ac.mdiq.podcini.util.Logd +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import android.widget.LinearLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.button.MaterialButtonToggleGroup +import org.apache.commons.lang3.StringUtils + +class FeedFilterDialog : BottomSheetDialogFragment() { + private lateinit var rows: LinearLayout + private var _binding: FilterDialogBinding? = null + private val binding get() = _binding!! + + var filter: FeedFilter? = null + private val buttonMap: MutableMap = mutableMapOf() + + private val newFilterValues: Set + get() { + val newFilterValues: MutableSet = HashSet() + for (i in 0 until rows.childCount) { + if (rows.getChildAt(i) !is MaterialButtonToggleGroup) continue + val group = rows.getChildAt(i) as MaterialButtonToggleGroup + if (group.checkedButtonId == View.NO_ID) continue + val tag = group.findViewById(group.checkedButtonId).tag as? String ?: continue + newFilterValues.add(tag) + } + return newFilterValues + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val layout = inflater.inflate(R.layout.filter_dialog, null, false) + _binding = FilterDialogBinding.bind(layout) + rows = binding.filterRows + Logd("FeedFilterDialog", "fragment onCreateView") + + //add filter rows + for (item in FeedFilterGroup.entries) { +// Logd("EpisodeFilterDialog", "FeedItemFilterGroup: ${item.values[0].filterId} ${item.values[1].filterId}") + val rBinding = FilterDialogRowBinding.inflate(inflater) +// rowBinding.root.addOnButtonCheckedListener { _: MaterialButtonToggleGroup?, _: Int, _: Boolean -> +// onFilterChanged(newFilterValues) +// } + rBinding.filterButton1.setOnClickListener { onFilterChanged(newFilterValues) } + rBinding.filterButton2.setOnClickListener { onFilterChanged(newFilterValues) } + + rBinding.filterButton1.setText(item.values[0].displayName) + rBinding.filterButton1.tag = item.values[0].filterId + buttonMap[item.values[0].filterId] = rBinding.filterButton1 + rBinding.filterButton2.setText(item.values[1].displayName) + rBinding.filterButton2.tag = item.values[1].filterId + buttonMap[item.values[1].filterId] = rBinding.filterButton2 + rBinding.filterButton1.maxLines = 3 + rBinding.filterButton1.isSingleLine = false + rBinding.filterButton2.maxLines = 3 + rBinding.filterButton2.isSingleLine = false + rows.addView(rBinding.root, rows.childCount - 1) + } + + binding.confirmFiltermenu.setOnClickListener { dismiss() } + binding.resetFiltermenu.setOnClickListener { + onFilterChanged(emptySet()) + for (i in 0 until rows.childCount) { + if (rows.getChildAt(i) is MaterialButtonToggleGroup) (rows.getChildAt(i) as MaterialButtonToggleGroup).clearChecked() + } + } + + if (filter != null) { + for (filterId in filter!!.values) { + if (filterId.isNotEmpty()) { + val button = buttonMap[filterId] + if (button != null) (button.parent as MaterialButtonToggleGroup).check(button.id) + } + } + } + return layout + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.setOnShowListener { dialogInterface: DialogInterface -> + val bottomSheetDialog = dialogInterface as BottomSheetDialog + setupFullHeight(bottomSheetDialog) + } + return dialog + } + + override fun onDestroyView() { + Logd(TAG, "onDestroyView") + _binding = null + super.onDestroyView() + } + + private fun setupFullHeight(bottomSheetDialog: BottomSheetDialog) { + val bottomSheet = bottomSheetDialog.findViewById(com.leinardi.android.speeddial.R.id.design_bottom_sheet) as? FrameLayout + if (bottomSheet != null) { + val behavior = BottomSheetBehavior.from(bottomSheet) + val layoutParams = bottomSheet.layoutParams + bottomSheet.layoutParams = layoutParams + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + fun onFilterChanged(newFilterValues: Set) { + feedsFilter = StringUtils.join(newFilterValues, ",") + Logd(TAG, "onFilterChanged: $feedsFilter") + EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues)) + } + + enum class FeedFilterGroup(vararg values: ItemProperties) { + KEEP_UPDATED(ItemProperties(R.string.keep_updated, FeedFilter.States.keepUpdated.name), ItemProperties(R.string.not_keep_updated, FeedFilter.States.not_keepUpdated.name)), + PLAY_SPEED(ItemProperties(R.string.global_speed, FeedFilter.States.global_playSpeed.name), ItemProperties(R.string.custom_speed, FeedFilter.States.custom_playSpeed.name)), + SKIPS(ItemProperties(R.string.has_skips, FeedFilter.States.has_skips.name), ItemProperties(R.string.no_skips, FeedFilter.States.no_skips.name)), + AUTO_DELETE(ItemProperties(R.string.always_auto_delete, FeedFilter.States.always_auto_delete.name), ItemProperties(R.string.never_auto_delete, FeedFilter.States.never_auto_delete.name)), + AUTO_DOWNLOAD(ItemProperties(R.string.auto_download, FeedFilter.States.autoDownload.name), ItemProperties(R.string.not_auto_download, FeedFilter.States.not_autoDownload.name)); + + @JvmField + val values: Array = arrayOf(*values) + + class ItemProperties(@JvmField val displayName: Int, @JvmField val filterId: String) + } + + companion object { + fun newInstance(filter: FeedFilter?): FeedFilterDialog { + val dialog = FeedFilterDialog() + dialog.filter = filter + return dialog + } + } +} 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 6627658c..4b57cce8 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 @@ -329,6 +329,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar val media = event.media if (currentMedia?.getIdentifier() == null || media?.getIdentifier() != currentMedia?.getIdentifier()) { currentMedia = media + playerUI?.updateUi(currentMedia) playerDetailsFragment?.setItem(curEpisode!!) } playerUI?.onPositionUpdate(event) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index 6c01a3fb..9e865c5f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -131,6 +131,7 @@ class FeedSettingsFragment : Fragment() { override fun onDestroyView() { Logd(TAG, "onDestroyView") feed = null + feedPrefs = null super.onDestroyView() } private fun setupFeedAutoSkipPreference() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index 8ea479df..85bff499 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -19,10 +19,8 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.database.RealmDB.upsert -import ac.mdiq.podcini.storage.model.Episode -import ac.mdiq.podcini.storage.model.EpisodeFilter -import ac.mdiq.podcini.storage.model.EpisodeMedia -import ac.mdiq.podcini.storage.model.EpisodeSortOrder +import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk +import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler @@ -49,7 +47,10 @@ import android.content.SharedPreferences import android.os.Bundle import android.util.Log import android.view.* +import android.widget.AdapterView +import android.widget.ArrayAdapter import android.widget.CheckBox +import android.widget.Spinner import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -111,6 +112,23 @@ import java.util.* displayUpArrow = parentFragmentManager.backStackEntryCount != 0 if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) + val queues = realm.query(PlayQueue::class).find() + val queueNames = queues.map { it.name }.toTypedArray() + val spinnerLayout = inflater.inflate(R.layout.queue_title_spinner, null) + val spinner = spinnerLayout.findViewById(R.id.queue_spinner) + toolbar.addView(spinnerLayout) + val sAdaptor = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, queueNames) + sAdaptor.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + spinner.adapter = sAdaptor + spinner.setSelection(sAdaptor.getPosition(curQueue.name)) + spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + curQueue = unmanaged(upsertBlk(queues[position]) { it.updated }) + loadItems(true) + } + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) toolbar.inflateMenu(R.menu.queue) refreshToolbarState() @@ -324,7 +342,7 @@ import java.util.* for (downloadUrl in event.urls) { val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), downloadUrl) if (pos >= 0) { - val item = queueItems[pos] + val item = unmanaged(queueItems[pos]) // item.media?.downloaded = true item.media?.setIsDownloaded() adapter?.notifyItemChangedCompat(pos) @@ -535,7 +553,7 @@ import java.util.* info += DurationConverter.getDurationStringLocalized(requireActivity(), timeLeft) } binding.infoBar.text = info - toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}" +// toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}" } private var loadItemsRunning = false 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 3653b44e..b7e385b8 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 @@ -9,17 +9,13 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.database.Feeds.getTags import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.RealmDB.realm -import ac.mdiq.podcini.storage.model.Episode -import ac.mdiq.podcini.storage.model.Feed -import ac.mdiq.podcini.storage.model.FeedPreferences -import ac.mdiq.podcini.storage.model.FeedSortOrder +import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.ui.actions.menuhandler.FeedMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.adapter.SelectableAdapter -import ac.mdiq.podcini.ui.dialog.FeedSortDialog -import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog -import ac.mdiq.podcini.ui.dialog.TagSettingsDialog +import ac.mdiq.podcini.ui.dialog.* +import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.FeedEpisodeFilterDialog import ac.mdiq.podcini.ui.utils.CoverLoader import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.LiftOnScrollListener @@ -134,7 +130,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { tagFilterIndex = position - filterOnTag() +// filterOnTag() + loadSubscriptions() } override fun onNothingSelected(parent: AdapterView<*>?) {} } @@ -224,24 +221,41 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec super.onSaveInstanceState(outState) } - fun filterOnTag() { - when (tagFilterIndex) { - 1 -> feedListFiltered = feedList // All feeds - 0 -> feedListFiltered = feedList.filter { // feeds without tag - val tags = it.preferences?.tags - tags.isNullOrEmpty() || (tags.size == 1 && tags.toList()[0] == "#root") - } + fun queryStringOfTags() : String { + return when (tagFilterIndex) { + 1 -> "" // All feeds + 0 -> " preferences.tags.@count == 0 OR (preferences.tags.@count == 0 AND ALL preferences.tags == '#root' ) " else -> { // feeds with the chosen tag val tag = tags[tagFilterIndex] - feedListFiltered = feedList.filter { - it.preferences?.tags?.contains(tag) ?: false - } + " ANY preferences.tags == '$tag' " } } + } + + fun filterOnTag() { + feedListFiltered = feedList binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() adapter.setItems(feedListFiltered) } +// fun filterOnTag() { +// when (tagFilterIndex) { +// 1 -> feedListFiltered = feedList // All feeds +// 0 -> feedListFiltered = feedList.filter { // feeds without tag +// val tags = it.preferences?.tags +// tags.isNullOrEmpty() || (tags.size == 1 && tags.toList()[0] == "#root") +// } +// else -> { // feeds with the chosen tag +// val tag = tags[tagFilterIndex] +// feedListFiltered = feedList.filter { +// it.preferences?.tags?.contains(tag) ?: false +// } +// } +// } +// binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() +// adapter.setItems(feedListFiltered) +// } + private fun resetTags() { tags.clear() tags.add("Untagged") @@ -263,6 +277,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec Logd(TAG, "Received event: ${event.TAG}") when (event) { is FlowEvent.FeedListEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions() + is FlowEvent.FeedsFilterEvent -> loadSubscriptions() is FlowEvent.EpisodePlayedEvent -> loadSubscriptions() is FlowEvent.FeedTagsChangedEvent -> loadSubscriptions() // is FlowEvent.FeedPrefsChangeEvent -> onFeedPrefsChangeEvent(event) @@ -293,6 +308,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { val itemId = item.itemId when (itemId) { + R.id.subscriptions_filter -> FeedFilterDialog.newInstance(FeedFilter(feedsFilter)).show(childFragmentManager, null) R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) R.id.subscriptions_sort -> FeedSortDialog().show(childFragmentManager, "FeedSortDialog") R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) @@ -318,7 +334,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec lifecycleScope.launch { try { withContext(Dispatchers.IO) { - sortFeeds() + filterAndSort() resetTags() } withContext(Dispatchers.Main) { @@ -328,6 +344,15 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec binding.progressBar.visibility = View.GONE adapter.setItems(feedListFiltered) binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() + if (feedsFilter.isNotEmpty()) { + val filter = FeedFilter(feedsFilter) + binding.txtvInformation.text = ("{gmo-info} " + getString(R.string.filtered_label)) + binding.txtvInformation.setOnClickListener { + val dialog = FeedFilterDialog.newInstance(filter) + dialog.show(childFragmentManager, null) + } + binding.txtvInformation.visibility = View.VISIBLE + } else binding.txtvInformation.visibility = View.GONE emptyView.updateVisibility() } } catch (e: Throwable) { @@ -339,13 +364,17 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } } - private fun sortFeeds() { - Logd(TAG, "sortFeeds() called") + private fun filterAndSort() { + val tagsQueryStr = queryStringOfTags() + val fQueryStr = if (tagsQueryStr.isEmpty()) FeedFilter(feedsFilter).queryString() else FeedFilter(feedsFilter).queryString() + " AND " + tagsQueryStr + Logd(TAG, "sortFeeds() called $feedsFilter $fQueryStr") + val feedIds = getFeedList(fQueryStr).map { id } val feedOrder = feedOrderBy val dir = 1 - 2*feedOrderDir // get from 0, 1 to 1, -1 val comparator: Comparator = when (feedOrder) { FeedSortOrder.UNPLAYED_NEW_OLD.index -> { - val episodes = realm.query(Episode::class).query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})").find() + val queryString = "feedId IN $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})" + val episodes = realm.query(Episode::class).query(queryString, feedIds).find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } @@ -361,12 +390,14 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } } FeedSortOrder.MOST_PLAYED.index -> { - val episodes = realm.query(Episode::class).query("playState == ${Episode.PLAYED}").find() + val queryString = "feedId IN $0 AND playState == ${Episode.PLAYED}" + val episodes = realm.query(Episode::class).query(queryString, feedIds).find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } FeedSortOrder.LAST_UPDATED_NEW_OLD.index -> { - val episodes = realm.query(Episode::class).sort("pubDate", Sort.DESCENDING).find() + val queryString = "feedId IN $0" + val episodes = realm.query(Episode::class, queryString, feedIds).sort("pubDate", Sort.DESCENDING).find() val counterMap: MutableMap = mutableMapOf() for (episode in episodes) { val feedId = episode.feedId ?: continue @@ -376,7 +407,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec comparator(counterMap, dir) } FeedSortOrder.LAST_DOWNLOAD_NEW_OLD.index -> { - val episodes = realm.query(Episode::class).sort("media.downloadTime", Sort.DESCENDING).find() + val queryString = "feedId IN $0" + val episodes = realm.query(Episode::class, queryString, feedIds).sort("media.downloadTime", Sort.DESCENDING).find() val counterMap: MutableMap = mutableMapOf() for (episode in episodes) { val feedId = episode.feedId ?: continue @@ -386,8 +418,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec comparator(counterMap, dir) } FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> { - val episodes = realm.query(Episode::class) - .query("playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}").find() + val queryString = "feedId IN $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})" + val episodes = realm.query(Episode::class).query(queryString, feedIds).find() val counterMap: MutableMap = mutableMapOf() for (episode in episodes) { val feedId = episode.feedId ?: continue @@ -397,24 +429,26 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec comparator(counterMap, dir) } FeedSortOrder.MOST_DOWNLOADED.index -> { - val episodes = realm.query(Episode::class).query("media.downloaded == true").find() + val queryString = "feedId IN $0 AND media.downloaded == true" + val episodes = realm.query(Episode::class).query(queryString, feedIds).find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> { - val episodes = realm.query(Episode::class) - .query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true").find() + val queryString = "feedId IN $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true" + val episodes = realm.query(Episode::class).query(queryString, feedIds).find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } // doing FEED_ORDER_NEW else -> { - val episodes = realm.query(Episode::class).query("playState == ${Episode.NEW}").find() + val queryString = "feedId IN $0 AND playState == ${Episode.NEW}" + val episodes = realm.query(Episode::class).query(queryString, feedIds).find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } } - val feedList_ = getFeedList().toMutableList() + val feedList_ = getFeedList(fQueryStr).toMutableList() synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() } } @@ -905,6 +939,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec return value } + var feedsFilter: String + get() = appPrefs.getString(UserPreferences.Prefs.prefFeedFilter.name, "")?:"" + set(filter) { + appPrefs.edit().putString(UserPreferences.Prefs.prefFeedFilter.name, filter).apply() + } + fun newInstance(folderTitle: String?): SubscriptionsFragment { val fragment = SubscriptionsFragment() val args = Bundle() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/event/FlowEvent.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/event/FlowEvent.kt index 5d3b1971..b41fe3b3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/event/FlowEvent.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/util/event/FlowEvent.kt @@ -75,6 +75,9 @@ sealed class FlowEvent { fun removed(episode: Episode): QueueEvent { return QueueEvent(Action.REMOVED, listOf(episode), -1) } + fun removed(episodes: List): QueueEvent { + return QueueEvent(Action.REMOVED, episodes, -1) + } fun irreversibleRemoved(episode: Episode): QueueEvent { return QueueEvent(Action.IRREVERSIBLE_REMOVED, listOf(episode), -1) } @@ -133,6 +136,8 @@ sealed class FlowEvent { data class FeedsSortedEvent(val dummy: Unit = Unit) : FlowEvent() + data class FeedsFilterEvent(val filterValues: Set?) : FlowEvent() + // data class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) : FlowEvent() // handled together in FeedPrefsChangeEvent diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml index 2cabcfbd..d85a3078 100644 --- a/app/src/main/res/layout/feeditemlist_item.xml +++ b/app/src/main/res/layout/feeditemlist_item.xml @@ -19,8 +19,8 @@ android:orientation="horizontal" android:gravity="center_vertical" android:baselineAligned="false" - android:paddingStart="12dp" - android:paddingLeft="12dp" + android:paddingStart="6dp" + android:paddingLeft="6dp" android:paddingEnd="0dp" android:paddingRight="0dp" tools:ignore="UselessParent"> @@ -53,8 +53,8 @@ android:layout_height="@dimen/thumbnail_length_queue_item" android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" - android:layout_marginRight="@dimen/listitem_threeline_textleftpadding" - android:layout_marginEnd="@dimen/listitem_threeline_textleftpadding" + android:layout_marginRight="@dimen/listitem_threeline_textrightpadding" + android:layout_marginEnd="@dimen/listitem_threeline_textrightpadding" app:cardBackgroundColor="@color/non_square_icon_background" app:cardCornerRadius="4dp" app:cardPreventCornerOverlap="false" diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml index 253c1a23..b454f6b3 100644 --- a/app/src/main/res/layout/fragment_subscriptions.xml +++ b/app/src/main/res/layout/fragment_subscriptions.xml @@ -43,6 +43,24 @@ android:layout_height="match_parent" android:layout_weight="1"/> + + + + + app:navigationIcon="?homeAsUpIndicator"/> + + + + + 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 1787e34e..f91d9f91 100644 --- a/app/src/main/res/menu/episodes_apply_action_speeddial.xml +++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml @@ -37,6 +37,11 @@ android:icon="@drawable/ic_playlist_play" android:title="@string/add_to_queue_label" /> + + + 16dp 16dp - 8dp - 8dp + 6dp + 5dp 8dp 16dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae2f7273..608db7e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,6 +146,8 @@ Newest unplayed Oldest unplayed + Put to queue + Nothing Never When not favorited @@ -854,6 +856,16 @@ Not played File name + Not keep updated + Global play speed + Custom play speed + Skips set + No Skips set + Always auto delete + Never auto delete + Auto download enabled + Auto download disabled + Include playback position Episode webpage diff --git a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt index 5e549afb..b18f6450 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt @@ -693,7 +693,7 @@ class DbWriterTest { // adapter.close() runBlocking { - val job = removeFromQueue(null, item) + val job = removeFromQueue(item) withTimeout(TIMEOUT*1000) { job.join() } } // adapter = getInstance() @@ -732,25 +732,25 @@ class DbWriterTest { val itemIds = toItemIds(feed.episodes).toTypedArray() runBlocking { - val job = removeFromQueue(null, feed.episodes[1], feed.episodes[3]) + val job = removeFromQueue(feed.episodes[1], feed.episodes[3]) withTimeout(TIMEOUT*1000) { job.join() } } assertQueueByItemIds("Average case - 2 items removed successfully", itemIds[0], itemIds[2]) runBlocking { - val job = removeFromQueue(null) + val job = removeFromQueue() withTimeout(TIMEOUT*1000) { job.join() } } assertQueueByItemIds("Boundary case - no items supplied. queue should see no change", itemIds[0], itemIds[2]) runBlocking { - val job = removeFromQueue(null, feed.episodes[0], feed.episodes[4]) + val job = removeFromQueue( feed.episodes[0], feed.episodes[4]) withTimeout(TIMEOUT*1000) { job.join() } } assertQueueByItemIds("Boundary case - items not in queue ignored", itemIds[2]) runBlocking { - val job = removeFromQueue(null, feed.episodes[2]) + val job = removeFromQueue( feed.episodes[2]) withTimeout(TIMEOUT*1000) { job.join() } } assertQueueByItemIds("Boundary case - invalid itemIds ignored") // the queue is empty diff --git a/changelog.md b/changelog.md index 3551d793..54ceead9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,10 +1,19 @@ +# 6.1.2 + +* fixed crash issue when setting the inclusive or exclusive filters in feed auto-download setting +* fixed player UI not updating on change of episode +* changed title of Queues view to a spinner for easily switching queues +* added "Put to queue" in multi-select menu putting selected episodes to a queue, this would also remove the episodes from any previous queues. +* added condition checks for preparing enqueuing sync actions +* in Subscriptions view added feeds filter based on feed preferences, in the same style as episodes filter + # 6.1.1 * fixed player UI not updating on change of episode * fixed the mal-function of restoring previously backed-up OPML * reduced reactions to PlaybackPositionEvent * tuned AutoCleanup a bit -* tuned and fixed some some issues in audo-downloaded +* tuned and fixed some some issues in auto-downloaded # 6.1.0 @@ -17,7 +26,7 @@ * skipped concurrent calls for loading data in multiple views * toggle "Auto backup of OPML" in Settings will restart Podcini * automatically restoring backup of OPML upon new install is disabled. Instead, in AddFeed view, when subscription is empty and OPML backup is available, a dialog is shown to ask about restoring. -* added audo downloadable to episodes filter +* added auto downloadable to episodes filter * added download date to episodes sorting * added download date to feed sorting * auto download algorithm is changed to individual feed based. diff --git a/fastlane/metadata/android/en-US/changelogs/3020216.txt b/fastlane/metadata/android/en-US/changelogs/3020216.txt new file mode 100644 index 00000000..7f3180c2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020216.txt @@ -0,0 +1,9 @@ + +Version 6.1.2 brings several changes: + +* fixed crash issue when setting the inclusive or exclusive filters in feed auto-download setting +* fixed player UI not updating on change of episode +* changed title of Queues view to a spinner for easily switching queues +* added "Put to queue" in multi-select menu putting selected episodes to a queue, this would also remove the episodes from any previous queues. +* added condition checks for preparing enqueuing sync actions +* in Subscriptions view added feeds filter based on feed preferences, in the same style as episodes filter