6.1.2 commit

This commit is contained in:
Xilin Jia 2024-07-20 20:41:46 +01:00
parent d47bdab971
commit 3c2618a29a
40 changed files with 656 additions and 224 deletions

View File

@ -24,8 +24,6 @@ Apache License 2.0
[com.squareup.okhttp3](https://github.com/square/okhttp/blob/master/LICENSE.txt) 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.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 [com.leinardi.android](https://github.com/leinardi/FloatingActionButtonSpeedDial/blob/release/LICENSE) Apache License 2.0

View File

@ -38,7 +38,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* More convenient player control displayed on all pages * More convenient player control displayed on all pages
* Revamped and more efficient expanded player view showing episode description on the front * 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 * 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 * 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 * 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 * 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) * 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 * 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: * Various efficiency improvements
* redundant media loadings and ui updates
* frequent list search during audio play
* streamed media somewhat equivalent to downloaded media * streamed media somewhat equivalent to downloaded media
* enabled episode description on player detailed view * enabled episode description on player detailed view
* enabled intro- and end- skipping * 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 * Played or new episodes have clearer markings
* Sort dialog no longer dims the main view * Sort dialog no longer dims the main view
* download date can be used to sort both feeds and episodes * 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 * 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) * 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 * 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 * 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 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 * 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 * 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 ### 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. * 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 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. * 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. * After auto download run, episodes with New status is changed to Unplayed.
* auto download feed setting dialog is also changed: * 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 * there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently

View File

@ -126,8 +126,8 @@ android {
buildConfig true buildConfig true
} }
defaultConfig { defaultConfig {
versionCode 3020215 versionCode 3020216
versionName "6.1.1" versionName "6.1.2"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -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.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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.net.utils.NetworkUtils.isAllowMobileEpisodeDownload
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs 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 val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result
workInfoList.forEach { workInfo -> workInfoList.forEach { workInfo ->
if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) { 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) WorkManager.getInstance(context).cancelAllWorkByTag(tag)
@ -383,7 +384,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message) Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message)
updatedStatus = DownloadResult(media.id, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, 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) val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD)
.currentTimestamp() .currentTimestamp()
.build() .build()

View File

@ -356,15 +356,9 @@ object FeedUpdateManager {
private const val serialVersionUID = 1L private const val serialVersionUID = 1L
} }
} }
companion object {
private val TAG: String = FeedParserTask::class.simpleName ?: "Anonymous"
}
} }
class FeedSyncTask(private val context: Context, request: DownloadRequest) { class FeedSyncTask(private val context: Context, request: DownloadRequest) {
// var savedFeed: Feed? = null
// private set
private val task = FeedParserTask(request) private val task = FeedParserTask(request)
private var feedHandlerResult: FeedHandlerResult? = null private var feedHandlerResult: FeedHandlerResult? = null
val downloadStatus: DownloadResult val downloadStatus: DownloadResult
@ -379,9 +373,5 @@ object FeedUpdateManager {
return true return true
} }
} }
companion object {
private val TAG: String = FeedUpdateWorker::class.simpleName ?: "Anonymous"
}
} }
} }

View File

@ -287,7 +287,7 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont
// if (result.first != null) queueToBeRemoved.add(result.second) // if (result.first != null) queueToBeRemoved.add(result.second)
updatedItems.add(result.second) updatedItems.add(result.second)
} }
removeFromQueue(null, *updatedItems.toTypedArray()) removeFromQueue(*updatedItems.toTypedArray())
// loadAdditionalFeedItemListData(updatedItems) // loadAdditionalFeedItemListData(updatedItems)
persistEpisodes(updatedItems) persistEpisodes(updatedItems)
} }

View File

@ -11,6 +11,10 @@ object SynchronizationQueueSink {
// To avoid a dependency loop of every class to SyncService, and from SyncService back to every class. // To avoid a dependency loop of every class to SyncService, and from SyncService back to every class.
private var serviceStarterImpl = Runnable {} private var serviceStarterImpl = Runnable {}
fun needSynch() : Boolean {
return isProviderConnected
}
fun setServiceStarterImpl(serviceStarter: Runnable) { fun setServiceStarterImpl(serviceStarter: Runnable) {
serviceStarterImpl = serviceStarter serviceStarterImpl = serviceStarter
} }
@ -57,9 +61,9 @@ object SynchronizationQueueSink {
fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) { fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) {
if (!isProviderConnected) return if (!isProviderConnected) return
if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return
val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY) val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY)
.currentTimestamp() .currentTimestamp()
.started(media.startPosition / 1000) .started(media.startPosition / 1000)

View File

@ -3,6 +3,7 @@ package ac.mdiq.podcini.playback.base
import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia
import ac.mdiq.podcini.storage.database.RealmDB.realm 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.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
@ -45,7 +46,7 @@ object InTheatre {
Logd(TAG, "starting curQueue") Logd(TAG, "starting curQueue")
var curQueue_ = realm.query(PlayQueue::class).sort("updated", Sort.DESCENDING).first().find() var curQueue_ = realm.query(PlayQueue::class).sort("updated", Sort.DESCENDING).first().find()
if (curQueue_ != null) { if (curQueue_ != null) {
curQueue = realm.copyFromRealm(curQueue_) curQueue = unmanaged(curQueue_)
curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds)
.find().sortedBy { curQueue.episodeIds.indexOf(it.id) })) .find().sortedBy { curQueue.episodeIds.indexOf(it.id) }))
} }
@ -68,7 +69,7 @@ object InTheatre {
Logd(TAG, "starting curState") Logd(TAG, "starting curState")
var curState_ = realm.query(CurrentState::class).first().find() var curState_ = realm.query(CurrentState::class).first().find()
if (curState_ != null) curState = realm.copyFromRealm(curState_) if (curState_ != null) curState = unmanaged(curState_)
else { else {
Logd(TAG, "creating new curState") Logd(TAG, "creating new curState")
curState_ = CurrentState() curState_ = CurrentState()

View File

@ -312,7 +312,7 @@ class PlaybackService : MediaSessionService() {
(action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!))) (action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!)))
if (playable is EpisodeMedia && shouldAutoDelete && (item?.isFavorite != true || !shouldFavoriteKeepEpisode())) { if (playable is EpisodeMedia && shouldAutoDelete && (item?.isFavorite != true || !shouldFavoriteKeepEpisode())) {
item = deleteMediaSync(this@PlaybackService, item!!) 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!!) if (playable is EpisodeMedia && (ended || skipped || playingNext)) addToHistory(item!!)

View File

@ -43,6 +43,8 @@ object UserPreferences {
prefDefaultPage, prefDefaultPage,
prefBackButtonOpensDrawer, prefBackButtonOpensDrawer,
prefFeedFilter,
prefQueueKeepSorted, prefQueueKeepSorted,
prefQueueKeepSortedOrder, prefQueueKeepSortedOrder,
prefDownloadSortedOrder, prefDownloadSortedOrder,

View File

@ -23,8 +23,9 @@ import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
object AutoCleanups { 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() get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodeCleanup.name, "" + EPISODE_CLEANUP_NULL)!!.toInt()
set(episodeCleanupValue) { set(episodeCleanupValue) {
appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply() appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply()
@ -102,9 +103,6 @@ object AutoCleanups {
} }
return 0 return 0
} }
companion object {
private val TAG: String = ExceptFavoriteCleanupAlgorithm::class.simpleName ?: "Anonymous"
}
} }
/** /**
@ -154,9 +152,6 @@ object AutoCleanups {
public override fun getDefaultCleanupParameter(): Int { public override fun getDefaultCleanupParameter(): Int {
return getNumEpisodesToCleanup(0) return getNumEpisodesToCleanup(0)
} }
companion object {
private val TAG: String = APQueueCleanupAlgorithm::class.simpleName ?: "Anonymous"
}
} }
/** /**
@ -174,9 +169,6 @@ object AutoCleanups {
override fun getReclaimableItems(): Int { override fun getReclaimableItems(): Int {
return 0 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. /** 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) return getNumEpisodesToCleanup(0)
} }
companion object { companion object {
private val TAG: String = APCleanupAlgorithm::class.simpleName ?: "Anonymous"
private fun minusHours(baseDate: Date, numberOfHours: Int): Date { private fun minusHours(baseDate: Date, numberOfHours: Int): Date {
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
cal.time = baseDate cal.time = baseDate

View File

@ -127,9 +127,6 @@ object AutoDownloads {
val status = batteryStatus!!.getIntExtra(BatteryManager.EXTRA_STATUS, -1) val status = batteryStatus!!.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
return (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL) return (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL)
} }
companion object {
private val TAG: String = AutoDownloadAlgorithm::class.simpleName ?: "Anonymous"
}
} }
class FeedBasedAutoDLAlgorithm : AutoDownloadAlgorithm() { class FeedBasedAutoDLAlgorithm : AutoDownloadAlgorithm() {
@ -141,10 +138,10 @@ object AutoDownloads {
val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload) val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload)
// true if we should auto download based on power status // true if we should auto download based on power status
val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery) 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 // we should only auto download if both network AND power are happy
if (networkShouldAutoDl && powerShouldAutoDl) { 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<Episode> = mutableSetOf() val candidates: MutableSet<Episode> = mutableSetOf()
val queueItems = realm.query(Episode::class).query("id IN $0 AND media.downloaded == false", curQueue.episodeIds).find() 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}") Logd(TAG, "autoDownloadEpisodeMedia add from queue: ${queueItems.size}")
@ -155,6 +152,7 @@ object AutoDownloads {
var episodes = mutableListOf<Episode>() var episodes = mutableListOf<Episode>()
val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name), f.id) val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name), f.id)
val allowedDLCount = (f.preferences?.autoDLMaxEpisodes?:0) - downloadedCount val allowedDLCount = (f.preferences?.autoDLMaxEpisodes?:0) - downloadedCount
Logd(TAG, "autoDownloadEpisodeMedia ${f.preferences?.autoDLMaxEpisodes} downloadedCount: $downloadedCount allowedDLCount: $allowedDLCount")
if (allowedDLCount > 0) { if (allowedDLCount > 0) {
var queryString = "feedId == ${f.id} AND isAutoDownloadEnabled == true AND media != nil AND media.downloaded == false" var queryString = "feedId == ${f.id} AND isAutoDownloadEnabled == true AND media != nil AND media.downloaded == false"
when (f.preferences?.autoDLPolicy) { when (f.preferences?.autoDLPolicy) {
@ -225,8 +223,5 @@ object AutoDownloads {
else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl") else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl")
} }
} }
companion object {
private val TAG: String = FeedBasedAutoDLAlgorithm::class.simpleName ?: "Anonymous"
}
} }
} }

View File

@ -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.feed.LocalFeedUpdater.updateFeed
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
@ -99,7 +100,7 @@ object Episodes {
return runOnIOScope { return runOnIOScope {
if (episode.media == null) return@runOnIOScope if (episode.media == null) return@runOnIOScope
val episode_ = deleteMediaSync(context, episode) 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 // Do full update of this feed to get rid of the episode
if (episode.feed != null) updateFeed(episode.feed!!, context.applicationContext, null) if (episode.feed != null) updateFeed(episode.feed!!, context.applicationContext, null)
} else { } else {
// Gpodder: queue delete action for synchronization if (needSynch()) {
val action = EpisodeAction.Builder(episode, EpisodeAction.DELETE).currentTimestamp().build() // Gpodder: queue delete action for synchronization
SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) val action = EpisodeAction.Builder(episode, EpisodeAction.DELETE).currentTimestamp().build()
SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action)
}
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(episode)) EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(episode))
} }
return episode return episode
@ -292,6 +295,6 @@ object Episodes {
} }
private fun shouldRemoveFromQueuesMarkPlayed(): Boolean { private fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
return appPrefs.getBoolean(UserPreferences.Prefs.prefRemoveFromQueueMarkedPlayed.name, true) return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
} }
} }

View File

@ -4,6 +4,7 @@ import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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.isAutoDelete
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
@ -39,8 +40,11 @@ object Feeds {
private val tags: MutableList<String> = mutableListOf() private val tags: MutableList<String> = mutableListOf()
@Synchronized @Synchronized
fun getFeedList(fromDB: Boolean = true): List<Feed> { fun getFeedList(queryString: String = "", fromDB: Boolean = true): List<Feed> {
if (fromDB) return realm.query(Feed::class).find() if (fromDB) {
return if (queryString.isEmpty()) realm.query(Feed::class).find()
else realm.query(Feed::class, queryString).find()
}
return feedMap.values.toList() return feedMap.values.toList()
} }
@ -278,7 +282,7 @@ object Feeds {
""".trimIndent())) """.trimIndent()))
oldItem.identifier = episode.identifier oldItem.identifier = episode.identifier
if (oldItem.isPlayed() && oldItem.media != null) { if (needSynch() && oldItem.isPlayed() && oldItem.media != null) {
val durs = oldItem.media!!.getDuration() / 1000 val durs = oldItem.media!!.getDuration() / 1000
val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY) val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY)
.currentTimestamp() .currentTimestamp()

View File

@ -15,7 +15,6 @@ import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
import android.content.Context
import android.util.Log import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
@ -29,6 +28,60 @@ object Queues {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM 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<Long> { fun getInQueueEpisodeIds(): Set<Long> {
Logd(TAG, "getQueueIDList() called") Logd(TAG, "getQueueIDList() called")
val queues = realm.query(PlayQueue::class).find() 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") Logd(TAG, "addToQueueSync( ... ) called")
val queue = queue_ ?: curQueue
val currentlyPlaying = curMedia val currentlyPlaying = curMedia
val positionCalculator = EnqueuePositionCalculator(enqueueLocation) 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) queue.episodeIds.add(insertPosition, episode.id)
curQueue.episodes.add(insertPosition, episode) queue.episodes.add(insertPosition, episode)
insertPosition++ insertPosition++
curQueue.update() queue.update()
upsert(curQueue) {} upsert(queue) {}
if (markAsUnplayed && episode.isNew) setPlayState(Episode.UNPLAYED, false, episode) 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) // if (performAutoDownload) autodownloadEpisodeMedia(context)
} }
@ -147,13 +201,11 @@ object Queues {
/** /**
* Removes a Episode object from the queue. * 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. * @param episodes FeedItems that should be removed.
*/ */
@OptIn(UnstableApi::class) @JvmStatic @OptIn(UnstableApi::class) @JvmStatic
fun removeFromQueue(context: Context?, vararg episodes: Episode) : Job { fun removeFromQueue(vararg episodes: Episode) : Job {
return runOnIOScope { removeFromQueueSync(curQueue, context, *episodes) } return runOnIOScope { removeFromQueueSync(curQueue, *episodes) }
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@ -161,22 +213,21 @@ object Queues {
Logd(TAG, "removeFromAllQueues called ") Logd(TAG, "removeFromAllQueues called ")
val queues = realm.query(PlayQueue::class).find() val queues = realm.query(PlayQueue::class).find()
for (q in queues) { 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 // ensure curQueue is last updated
removeFromQueueSync(curQueue, null, *episodes) removeFromQueueSync(curQueue, *episodes)
} }
/** /**
* @param queue_ if null, use curQueue * @param queue_ if null, use curQueue
* @param context perform autodownloadEpisodeMedia only if context is not null and queue_ is curQueue
*/ */
@UnstableApi @UnstableApi
internal fun removeFromQueueSync(queue_: PlayQueue?, context: Context?, vararg episodes: Episode) { internal fun removeFromQueueSync(queue_: PlayQueue?, vararg episodes: Episode) {
Logd(TAG, "removeFromQueueSync called ") Logd(TAG, "removeFromQueueSync called ")
if (episodes.isEmpty()) return if (episodes.isEmpty()) return
val queue = queue_ ?: curQueue var queue = queue_ ?: curQueue
val events: MutableList<FlowEvent.QueueEvent> = ArrayList() val events: MutableList<FlowEvent.QueueEvent> = ArrayList()
val pos: MutableList<Int> = mutableListOf() val pos: MutableList<Int> = mutableListOf()
val qItems = queue.episodes.toMutableList() val qItems = queue.episodes.toMutableList()
@ -200,9 +251,6 @@ object Queues {
} }
for (event in events) EventFlow.postEvent(event) for (event in events) EventFlow.postEvent(event)
} else Logd(TAG, "Queue was not modified by call to removeQueueItem") } 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<Long>) { suspend fun removeFromAllQueuesQuiet(episodeIds: List<Long>) {
@ -214,10 +262,11 @@ object Queues {
eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (eidsInQueues.isNotEmpty()) { if (eidsInQueues.isNotEmpty()) {
val qeids = q.episodeIds.minus(eidsInQueues) val qeids = q.episodeIds.minus(eidsInQueues)
q.episodeIds.clear() upsert(q) {
q.episodeIds.addAll(qeids) it.episodeIds.clear()
q.update() it.episodeIds.addAll(qeids)
upsert(q) {} it.update()
}
} }
} }
// ensure curQueue is last updated // ensure curQueue is last updated
@ -225,10 +274,11 @@ object Queues {
eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (eidsInQueues.isNotEmpty()) { if (eidsInQueues.isNotEmpty()) {
val qeids = q.episodeIds.minus(eidsInQueues) val qeids = q.episodeIds.minus(eidsInQueues)
q.episodeIds.clear() upsert(q) {
q.episodeIds.addAll(qeids) it.episodeIds.clear()
q.update() it.episodeIds.addAll(qeids)
upsert(q) {} it.update()
}
} }
} }
@ -270,60 +320,6 @@ object Queues {
upsertBlk(curQueue) {} 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) { class EnqueuePositionCalculator(private val enqueueLocation: EnqueueLocation) {
/** /**
* Determine the position (0-based) that the item(s) should be inserted to the named queue. * Determine the position (0-based) that the item(s) should be inserted to the named queue.

View File

@ -4,38 +4,22 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import java.io.Serializable import java.io.Serializable
class EpisodeFilter(vararg properties: String) : Serializable { class EpisodeFilter(vararg properties: String) : Serializable {
private val properties: Array<String> = arrayOf(*properties.filter { it.isNotEmpty() }.map {it.trim()}.toTypedArray()) private val properties: Array<String> = arrayOf(*properties.filter { it.isNotEmpty() }.map {it.trim()}.toTypedArray())
@JvmField
val showPlayed: Boolean = hasProperty(States.played.name) val showPlayed: Boolean = hasProperty(States.played.name)
@JvmField
val showUnplayed: Boolean = hasProperty(States.unplayed.name) val showUnplayed: Boolean = hasProperty(States.unplayed.name)
@JvmField
val showPaused: Boolean = hasProperty(States.paused.name) val showPaused: Boolean = hasProperty(States.paused.name)
@JvmField
val showNotPaused: Boolean = hasProperty(States.not_paused.name) val showNotPaused: Boolean = hasProperty(States.not_paused.name)
@JvmField
val showNew: Boolean = hasProperty(States.new.name) val showNew: Boolean = hasProperty(States.new.name)
@JvmField
val showQueued: Boolean = hasProperty(States.queued.name) val showQueued: Boolean = hasProperty(States.queued.name)
@JvmField
val showNotQueued: Boolean = hasProperty(States.not_queued.name) val showNotQueued: Boolean = hasProperty(States.not_queued.name)
@JvmField
val showDownloaded: Boolean = hasProperty(States.downloaded.name) val showDownloaded: Boolean = hasProperty(States.downloaded.name)
@JvmField
val showNotDownloaded: Boolean = hasProperty(States.not_downloaded.name) val showNotDownloaded: Boolean = hasProperty(States.not_downloaded.name)
@JvmField
val showAutoDownloadable: Boolean = hasProperty(States.auto_downloadable.name) val showAutoDownloadable: Boolean = hasProperty(States.auto_downloadable.name)
@JvmField
val showNotAutoDownloadable: Boolean = hasProperty(States.not_auto_downloadable.name) val showNotAutoDownloadable: Boolean = hasProperty(States.not_auto_downloadable.name)
@JvmField
val showHasMedia: Boolean = hasProperty(States.has_media.name) val showHasMedia: Boolean = hasProperty(States.has_media.name)
@JvmField
val showNoMedia: Boolean = hasProperty(States.no_media.name) val showNoMedia: Boolean = hasProperty(States.no_media.name)
@JvmField
val showIsFavorite: Boolean = hasProperty(States.is_favorite.name) val showIsFavorite: Boolean = hasProperty(States.is_favorite.name)
@JvmField
val showNotFavorite: Boolean = hasProperty(States.not_favorite.name) val showNotFavorite: Boolean = hasProperty(States.not_favorite.name)
constructor(properties: String) : this(*(properties.split(",").toTypedArray())) constructor(properties: String) : this(*(properties.split(",").toTypedArray()))
@ -115,6 +99,7 @@ class EpisodeFilter(vararg properties: String) : Serializable {
return query.toString() return query.toString()
} }
@Suppress("EnumEntryName")
enum class States { enum class States {
played, played,
unplayed, unplayed,

View File

@ -99,11 +99,11 @@ class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilt
} }
fun hasIncludeFilter(): Boolean { fun hasIncludeFilter(): Boolean {
return includeFilterRaw!!.isNotEmpty() return !includeFilterRaw.isNullOrEmpty()
} }
fun hasExcludeFilter(): Boolean { fun hasExcludeFilter(): Boolean {
return excludeFilterRaw!!.isNotEmpty() return !excludeFilterRaw.isNullOrEmpty()
} }
fun hasMinimalDurationFilter(): Boolean { fun hasMinimalDurationFilter(): Boolean {

View File

@ -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<String> = 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<String>
get() = properties.clone()
val valuesList: List<String>
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<String> = 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("")
}
}
}

View File

@ -1,19 +1,36 @@
package ac.mdiq.podcini.ui.actions 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.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.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
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode
import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.Queues 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.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.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 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 @UnstableApi
@ -26,6 +43,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
R.id.add_to_favorite_batch -> markFavorite(items, true) R.id.add_to_favorite_batch -> markFavorite(items, true)
R.id.remove_favorite_batch -> markFavorite(items, false) R.id.remove_favorite_batch -> markFavorite(items, false)
R.id.add_to_queue_batch -> queueChecked(items) 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.remove_from_queue_batch -> removeFromQueueChecked(items)
R.id.mark_read_batch -> { R.id.mark_read_batch -> {
setPlayState(Episode.PLAYED, false, *items.toTypedArray()) setPlayState(Episode.PLAYED, false, *items.toTypedArray())
@ -53,7 +71,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
private fun removeFromQueueChecked(items: List<Episode>) { private fun removeFromQueueChecked(items: List<Episode>) {
val checkedIds = getSelectedIds(items) val checkedIds = getSelectedIds(items)
removeFromQueue(activity, *items.toTypedArray()) removeFromQueue(*items.toTypedArray())
showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.size) showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.size)
} }
@ -96,6 +114,59 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
return checkedIds return checkedIds
} }
private fun putToQueue(items: List<Episode>) {
PutToQueueDialog(activity as MainActivity, items).show()
}
class PutToQueueDialog(activity: Activity, val items: List<Episode>) {
private val activityRef: WeakReference<Activity> = 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<Long>()
val toRemoveCur = mutableListOf<Episode>()
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 { companion object {
private val TAG: String = EpisodeMultiSelectHandler::class.simpleName ?: "Anonymous" private val TAG: String = EpisodeMultiSelectHandler::class.simpleName ?: "Anonymous"
} }

View File

@ -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.SynchronizationSettings.wifiSyncEnabledKey
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE
import ac.mdiq.podcini.receiver.MediaButtonReceiver 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.setFavorite
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.Queues.addToQueue import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
@ -145,7 +145,7 @@ object EpisodeMenuHandler {
if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) { if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) {
val media: EpisodeMedia? = selectedItem.media val media: EpisodeMedia? = selectedItem.media
// not all items have media, Gpodder only cares about those that do // 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) val actionPlay: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY)
.currentTimestamp() .currentTimestamp()
.started(media.getDuration() / 1000) .started(media.getDuration() / 1000)
@ -159,7 +159,7 @@ object EpisodeMenuHandler {
R.id.mark_unread_item -> { R.id.mark_unread_item -> {
selectedItem.setPlayed(false) selectedItem.setPlayed(false)
setPlayState(Episode.UNPLAYED, false, selectedItem) 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) val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp() .currentTimestamp()
.build() .build()
@ -167,7 +167,7 @@ object EpisodeMenuHandler {
} }
} }
R.id.add_to_queue_item -> addToQueue(true, selectedItem) 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.add_to_favorites_item -> setFavorite(selectedItem, true)
R.id.remove_from_favorites_item -> setFavorite(selectedItem, false) R.id.remove_from_favorites_item -> setFavorite(selectedItem, false)
R.id.reset_position -> { R.id.reset_position -> {

View File

@ -37,7 +37,7 @@ class RemoveFromQueueSwipeAction : SwipeAction {
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) { @OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
val position: Int = curQueue.episodes.indexOf(item) val position: Int = curQueue.episodes.indexOf(item)
removeFromQueue(fragment.requireActivity(), item) removeFromQueue(item)
if (willRemove(filter, 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) (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)) { .setAction(fragment.getString(R.string.undo)) {

View File

@ -52,7 +52,7 @@ class TogglePlaybackStateSwipeAction : SwipeAction {
val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!) val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) { if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
item = deleteMediaSync(fragment.requireContext(), item) item = deleteMediaSync(fragment.requireContext(), item)
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, null, item) } if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) }
} }
val playStateStringRes: Int = when (newState) { val playStateStringRes: Int = when (newState) {
Episode.UNPLAYED -> if (item.playState == Episode.NEW) R.string.removed_inbox_label //was new 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) { if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
// deleteMediaOfEpisode(fragment.requireContext(), item) // deleteMediaOfEpisode(fragment.requireContext(), item)
var item = deleteMediaSync(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 { override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {

View File

@ -45,7 +45,7 @@ abstract class EpisodeFilterDialog : BottomSheetDialogFragment() {
val layout = inflater.inflate(R.layout.filter_dialog, null, false) val layout = inflater.inflate(R.layout.filter_dialog, null, false)
_binding = FilterDialogBinding.bind(layout) _binding = FilterDialogBinding.bind(layout)
rows = binding.filterRows rows = binding.filterRows
Logd("ItemFilterDialog", "fragment onCreateView") Logd("EpisodeFilterDialog", "fragment onCreateView")
//add filter rows //add filter rows
for (item in FeedItemFilterGroup.entries) { for (item in FeedItemFilterGroup.entries) {

View File

@ -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<String, Button> = mutableMapOf()
private val newFilterValues: Set<String>
get() {
val newFilterValues: MutableSet<String> = 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<View>(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<View>(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<String>) {
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<ItemProperties> = 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
}
}
}

View File

@ -329,6 +329,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val media = event.media val media = event.media
if (currentMedia?.getIdentifier() == null || media?.getIdentifier() != currentMedia?.getIdentifier()) { if (currentMedia?.getIdentifier() == null || media?.getIdentifier() != currentMedia?.getIdentifier()) {
currentMedia = media currentMedia = media
playerUI?.updateUi(currentMedia)
playerDetailsFragment?.setItem(curEpisode!!) playerDetailsFragment?.setItem(curEpisode!!)
} }
playerUI?.onPositionUpdate(event) playerUI?.onPositionUpdate(event)

View File

@ -131,6 +131,7 @@ class FeedSettingsFragment : Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
Logd(TAG, "onDestroyView") Logd(TAG, "onDestroyView")
feed = null feed = null
feedPrefs = null
super.onDestroyView() super.onDestroyView()
} }
private fun setupFeedAutoSkipPreference() { private fun setupFeedAutoSkipPreference() {

View File

@ -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.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
@ -49,7 +47,10 @@ import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.Spinner
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -111,6 +112,23 @@ import java.util.*
displayUpArrow = parentFragmentManager.backStackEntryCount != 0 displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) 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<Spinner>(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) (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
toolbar.inflateMenu(R.menu.queue) toolbar.inflateMenu(R.menu.queue)
refreshToolbarState() refreshToolbarState()
@ -324,7 +342,7 @@ import java.util.*
for (downloadUrl in event.urls) { for (downloadUrl in event.urls) {
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), downloadUrl) val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), downloadUrl)
if (pos >= 0) { if (pos >= 0) {
val item = queueItems[pos] val item = unmanaged(queueItems[pos])
// item.media?.downloaded = true // item.media?.downloaded = true
item.media?.setIsDownloaded() item.media?.setIsDownloaded()
adapter?.notifyItemChangedCompat(pos) adapter?.notifyItemChangedCompat(pos)
@ -535,7 +553,7 @@ import java.util.*
info += DurationConverter.getDurationStringLocalized(requireActivity(), timeLeft) info += DurationConverter.getDurationStringLocalized(requireActivity(), timeLeft)
} }
binding.infoBar.text = info 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 private var loadItemsRunning = false

View File

@ -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.getTags
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.*
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.ui.actions.menuhandler.FeedMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.FeedMenuHandler
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.SelectableAdapter import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.dialog.FeedSortDialog import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.FeedEpisodeFilterDialog
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
import ac.mdiq.podcini.ui.utils.CoverLoader import ac.mdiq.podcini.ui.utils.CoverLoader
import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
@ -134,7 +130,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
tagFilterIndex = position tagFilterIndex = position
filterOnTag() // filterOnTag()
loadSubscriptions()
} }
override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onNothingSelected(parent: AdapterView<*>?) {}
} }
@ -224,24 +221,41 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
fun filterOnTag() { fun queryStringOfTags() : String {
when (tagFilterIndex) { return when (tagFilterIndex) {
1 -> feedListFiltered = feedList // All feeds 1 -> "" // All feeds
0 -> feedListFiltered = feedList.filter { // feeds without tag 0 -> " preferences.tags.@count == 0 OR (preferences.tags.@count == 0 AND ALL preferences.tags == '#root' ) "
val tags = it.preferences?.tags
tags.isNullOrEmpty() || (tags.size == 1 && tags.toList()[0] == "#root")
}
else -> { // feeds with the chosen tag else -> { // feeds with the chosen tag
val tag = tags[tagFilterIndex] val tag = tags[tagFilterIndex]
feedListFiltered = feedList.filter { " ANY preferences.tags == '$tag' "
it.preferences?.tags?.contains(tag) ?: false
}
} }
} }
}
fun filterOnTag() {
feedListFiltered = feedList
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
adapter.setItems(feedListFiltered) 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() { private fun resetTags() {
tags.clear() tags.clear()
tags.add("Untagged") tags.add("Untagged")
@ -263,6 +277,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
Logd(TAG, "Received event: ${event.TAG}") Logd(TAG, "Received event: ${event.TAG}")
when (event) { when (event) {
is FlowEvent.FeedListEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions() is FlowEvent.FeedListEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions()
is FlowEvent.FeedsFilterEvent -> loadSubscriptions()
is FlowEvent.EpisodePlayedEvent -> loadSubscriptions() is FlowEvent.EpisodePlayedEvent -> loadSubscriptions()
is FlowEvent.FeedTagsChangedEvent -> loadSubscriptions() is FlowEvent.FeedTagsChangedEvent -> loadSubscriptions()
// is FlowEvent.FeedPrefsChangeEvent -> onFeedPrefsChangeEvent(event) // is FlowEvent.FeedPrefsChangeEvent -> onFeedPrefsChangeEvent(event)
@ -293,6 +308,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
@UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean {
val itemId = item.itemId val itemId = item.itemId
when (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.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
R.id.subscriptions_sort -> FeedSortDialog().show(childFragmentManager, "FeedSortDialog") R.id.subscriptions_sort -> FeedSortDialog().show(childFragmentManager, "FeedSortDialog")
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext())
@ -318,7 +334,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
lifecycleScope.launch { lifecycleScope.launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
sortFeeds() filterAndSort()
resetTags() resetTags()
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -328,6 +344,15 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
adapter.setItems(feedListFiltered) adapter.setItems(feedListFiltered)
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() 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() emptyView.updateVisibility()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -339,13 +364,17 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
} }
} }
private fun sortFeeds() { private fun filterAndSort() {
Logd(TAG, "sortFeeds() called") 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 feedOrder = feedOrderBy
val dir = 1 - 2*feedOrderDir // get from 0, 1 to 1, -1 val dir = 1 - 2*feedOrderDir // get from 0, 1 to 1, -1
val comparator: Comparator<Feed> = when (feedOrder) { val comparator: Comparator<Feed> = when (feedOrder) {
FeedSortOrder.UNPLAYED_NEW_OLD.index -> { 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) val counterMap = counterMap(episodes)
comparator(counterMap, dir) comparator(counterMap, dir)
} }
@ -361,12 +390,14 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
} }
} }
FeedSortOrder.MOST_PLAYED.index -> { 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) val counterMap = counterMap(episodes)
comparator(counterMap, dir) comparator(counterMap, dir)
} }
FeedSortOrder.LAST_UPDATED_NEW_OLD.index -> { 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<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (episode in episodes) { for (episode in episodes) {
val feedId = episode.feedId ?: continue val feedId = episode.feedId ?: continue
@ -376,7 +407,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
comparator(counterMap, dir) comparator(counterMap, dir)
} }
FeedSortOrder.LAST_DOWNLOAD_NEW_OLD.index -> { 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<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (episode in episodes) { for (episode in episodes) {
val feedId = episode.feedId ?: continue val feedId = episode.feedId ?: continue
@ -386,8 +418,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
comparator(counterMap, dir) comparator(counterMap, dir)
} }
FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> { FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> {
val episodes = realm.query(Episode::class) val queryString = "feedId IN $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})"
.query("playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}").find() val episodes = realm.query(Episode::class).query(queryString, feedIds).find()
val counterMap: MutableMap<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (episode in episodes) { for (episode in episodes) {
val feedId = episode.feedId ?: continue val feedId = episode.feedId ?: continue
@ -397,24 +429,26 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
comparator(counterMap, dir) comparator(counterMap, dir)
} }
FeedSortOrder.MOST_DOWNLOADED.index -> { 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) val counterMap = counterMap(episodes)
comparator(counterMap, dir) comparator(counterMap, dir)
} }
FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> { FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> {
val episodes = realm.query(Episode::class) val queryString = "feedId IN $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true"
.query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true").find() val episodes = realm.query(Episode::class).query(queryString, feedIds).find()
val counterMap = counterMap(episodes) val counterMap = counterMap(episodes)
comparator(counterMap, dir) comparator(counterMap, dir)
} }
// doing FEED_ORDER_NEW // doing FEED_ORDER_NEW
else -> { 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) val counterMap = counterMap(episodes)
comparator(counterMap, dir) comparator(counterMap, dir)
} }
} }
val feedList_ = getFeedList().toMutableList() val feedList_ = getFeedList(fQueryStr).toMutableList()
synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() } synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() }
} }
@ -905,6 +939,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
return value 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 { fun newInstance(folderTitle: String?): SubscriptionsFragment {
val fragment = SubscriptionsFragment() val fragment = SubscriptionsFragment()
val args = Bundle() val args = Bundle()

View File

@ -75,6 +75,9 @@ sealed class FlowEvent {
fun removed(episode: Episode): QueueEvent { fun removed(episode: Episode): QueueEvent {
return QueueEvent(Action.REMOVED, listOf(episode), -1) return QueueEvent(Action.REMOVED, listOf(episode), -1)
} }
fun removed(episodes: List<Episode>): QueueEvent {
return QueueEvent(Action.REMOVED, episodes, -1)
}
fun irreversibleRemoved(episode: Episode): QueueEvent { fun irreversibleRemoved(episode: Episode): QueueEvent {
return QueueEvent(Action.IRREVERSIBLE_REMOVED, listOf(episode), -1) 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 FeedsSortedEvent(val dummy: Unit = Unit) : FlowEvent()
data class FeedsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
// data class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) : FlowEvent() // data class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) : FlowEvent()
// handled together in FeedPrefsChangeEvent // handled together in FeedPrefsChangeEvent

View File

@ -19,8 +19,8 @@
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:baselineAligned="false" android:baselineAligned="false"
android:paddingStart="12dp" android:paddingStart="6dp"
android:paddingLeft="12dp" android:paddingLeft="6dp"
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:paddingRight="0dp" android:paddingRight="0dp"
tools:ignore="UselessParent"> tools:ignore="UselessParent">
@ -53,8 +53,8 @@
android:layout_height="@dimen/thumbnail_length_queue_item" android:layout_height="@dimen/thumbnail_length_queue_item"
android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding"
android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" android:layout_marginTop="@dimen/listitem_threeline_verticalpadding"
android:layout_marginRight="@dimen/listitem_threeline_textleftpadding" android:layout_marginRight="@dimen/listitem_threeline_textrightpadding"
android:layout_marginEnd="@dimen/listitem_threeline_textleftpadding" android:layout_marginEnd="@dimen/listitem_threeline_textrightpadding"
app:cardBackgroundColor="@color/non_square_icon_background" app:cardBackgroundColor="@color/non_square_icon_background"
app:cardCornerRadius="4dp" app:cardCornerRadius="4dp"
app:cardPreventCornerOverlap="false" app:cardPreventCornerOverlap="false"

View File

@ -43,6 +43,24 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1"/> android:layout_weight="1"/>
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/txtvInformation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp"
android:background="?android:attr/colorBackground"
android:foreground="?android:attr/selectableItemBackground"
android:visibility="gone"
android:gravity="center"
android:textColor="?attr/colorAccent"
tools:visibility="visible"
tools:text="(i) Information" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<TextView <TextView
android:id="@+id/count" android:id="@+id/count"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -19,8 +19,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize" android:layout_height="?android:attr/actionBarSize"
app:navigationContentDescription="@string/toolbar_back_button_content_description" app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator" app:navigationIcon="?homeAsUpIndicator"/>
app:title="@string/queue_label" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/queue_title_spinner">
<Spinner
android:id="@+id/queue_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:dropDownWidth="100dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_large"
android:textStyle="bold"
android:spinnerMode="dropdown"/>
</LinearLayout>

View File

@ -37,6 +37,11 @@
android:icon="@drawable/ic_playlist_play" android:icon="@drawable/ic_playlist_play"
android:title="@string/add_to_queue_label" /> android:title="@string/add_to_queue_label" />
<item
android:id="@+id/put_to_queue_batch"
android:icon="@drawable/ic_playlist_play"
android:title="@string/put_to_queue_label" />
<item <item
android:id="@+id/add_to_favorite_batch" android:id="@+id/add_to_favorite_batch"
android:icon="@drawable/ic_star" android:icon="@drawable/ic_star"

View File

@ -6,6 +6,12 @@
android:icon="@drawable/ic_search" android:icon="@drawable/ic_search"
custom:showAsAction="always" custom:showAsAction="always"
android:title="@string/search_label"/> android:title="@string/search_label"/>
<item
android:id="@+id/subscriptions_filter"
android:icon="@drawable/ic_filter"
android:menuCategory="container"
android:title="@string/filter"
custom:showAsAction="always"/>
<item <item
android:id="@+id/subscriptions_sort" android:id="@+id/subscriptions_sort"
android:title="@string/sort" android:title="@string/sort"

View File

@ -20,8 +20,8 @@
<dimen name="listitem_iconwithtext_textleftpadding">16dp</dimen> <dimen name="listitem_iconwithtext_textleftpadding">16dp</dimen>
<dimen name="listitem_threeline_textleftpadding">16dp</dimen> <dimen name="listitem_threeline_textleftpadding">16dp</dimen>
<dimen name="listitem_threeline_textrightpadding">8dp</dimen> <dimen name="listitem_threeline_textrightpadding">6dp</dimen>
<dimen name="listitem_threeline_verticalpadding">8dp</dimen> <dimen name="listitem_threeline_verticalpadding">5dp</dimen>
<dimen name="list_vertical_padding">8dp</dimen> <dimen name="list_vertical_padding">8dp</dimen>
<dimen name="listitem_icon_leftpadding">16dp</dimen> <dimen name="listitem_icon_leftpadding">16dp</dimen>

View File

@ -146,6 +146,8 @@
<string name="feed_auto_download_newer">Newest unplayed</string> <string name="feed_auto_download_newer">Newest unplayed</string>
<string name="feed_auto_download_older">Oldest unplayed</string> <string name="feed_auto_download_older">Oldest unplayed</string>
<string name="put_to_queue_label">Put to queue</string>
<string name="feed_new_episodes_action_nothing">Nothing</string> <string name="feed_new_episodes_action_nothing">Nothing</string>
<string name="episode_cleanup_never">Never</string> <string name="episode_cleanup_never">Never</string>
<string name="episode_cleanup_except_favorite_removal">When not favorited</string> <string name="episode_cleanup_except_favorite_removal">When not favorited</string>
@ -854,6 +856,16 @@
<string name="not_played">Not played</string> <string name="not_played">Not played</string>
<string name="filename">File name</string> <string name="filename">File name</string>
<string name="not_keep_updated">Not keep updated</string>
<string name="global_speed">Global play speed</string>
<string name="custom_speed">Custom play speed</string>
<string name="has_skips">Skips set</string>
<string name="no_skips">No Skips set</string>
<string name="always_auto_delete">Always auto delete</string>
<string name="never_auto_delete">Never auto delete</string>
<string name="auto_download">Auto download enabled</string>
<string name="not_auto_download">Auto download disabled</string>
<!-- Share episode dialog --> <!-- Share episode dialog -->
<string name="share_playback_position_dialog_label">Include playback position</string> <string name="share_playback_position_dialog_label">Include playback position</string>
<string name="share_dialog_episode_website_label">Episode webpage</string> <string name="share_dialog_episode_website_label">Episode webpage</string>

View File

@ -693,7 +693,7 @@ class DbWriterTest {
// adapter.close() // adapter.close()
runBlocking { runBlocking {
val job = removeFromQueue(null, item) val job = removeFromQueue(item)
withTimeout(TIMEOUT*1000) { job.join() } withTimeout(TIMEOUT*1000) { job.join() }
} }
// adapter = getInstance() // adapter = getInstance()
@ -732,25 +732,25 @@ class DbWriterTest {
val itemIds = toItemIds(feed.episodes).toTypedArray<Long>() val itemIds = toItemIds(feed.episodes).toTypedArray<Long>()
runBlocking { 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() } withTimeout(TIMEOUT*1000) { job.join() }
} }
assertQueueByItemIds("Average case - 2 items removed successfully", itemIds[0], itemIds[2]) assertQueueByItemIds("Average case - 2 items removed successfully", itemIds[0], itemIds[2])
runBlocking { runBlocking {
val job = removeFromQueue(null) val job = removeFromQueue()
withTimeout(TIMEOUT*1000) { job.join() } withTimeout(TIMEOUT*1000) { job.join() }
} }
assertQueueByItemIds("Boundary case - no items supplied. queue should see no change", itemIds[0], itemIds[2]) assertQueueByItemIds("Boundary case - no items supplied. queue should see no change", itemIds[0], itemIds[2])
runBlocking { 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() } withTimeout(TIMEOUT*1000) { job.join() }
} }
assertQueueByItemIds("Boundary case - items not in queue ignored", itemIds[2]) assertQueueByItemIds("Boundary case - items not in queue ignored", itemIds[2])
runBlocking { runBlocking {
val job = removeFromQueue(null, feed.episodes[2]) val job = removeFromQueue( feed.episodes[2])
withTimeout(TIMEOUT*1000) { job.join() } withTimeout(TIMEOUT*1000) { job.join() }
} }
assertQueueByItemIds("Boundary case - invalid itemIds ignored") // the queue is empty assertQueueByItemIds("Boundary case - invalid itemIds ignored") // the queue is empty

View File

@ -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 # 6.1.1
* fixed player UI not updating on change of episode * fixed player UI not updating on change of episode
* fixed the mal-function of restoring previously backed-up OPML * fixed the mal-function of restoring previously backed-up OPML
* reduced reactions to PlaybackPositionEvent * reduced reactions to PlaybackPositionEvent
* tuned AutoCleanup a bit * 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 # 6.1.0
@ -17,7 +26,7 @@
* skipped concurrent calls for loading data in multiple views * skipped concurrent calls for loading data in multiple views
* toggle "Auto backup of OPML" in Settings will restart Podcini * 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. * 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 episodes sorting
* added download date to feed sorting * added download date to feed sorting
* auto download algorithm is changed to individual feed based. * auto download algorithm is changed to individual feed based.

View File

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