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
[//]: # ([io.reactivex.rxjava2](https://github.com/ReactiveX/RxJava/blob/3.x/LICENSE) Apache License 2.0)
[com.mikepenz:iconics-core](https://github.com/mikepenz/Android-Iconics/blob/develop/LICENSE) Apache License 2.0
[com.leinardi.android](https://github.com/leinardi/FloatingActionButtonSpeedDial/blob/release/LICENSE) Apache License 2.0

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
* Revamped and more efficient expanded player view showing episode description on the front
* External player class is merged into the player
* Playback speed setting has been straightened up, three speed can be set separately or combined: current audio, podcast, and global
* Added preference "Fast Forward Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a number between 0.0 and 10.0
* The "Skip to next episode" button on the player
@ -50,9 +49,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* single tap not during play has no effect
* Added preference "Fallback Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 1.5)
* if the user customizes "Fallback speed" to a value greater than 0.1, long-press the Play button during play enters the fallback mode and plays at the set fallback speed, single tap exits the fallback mode
* Various efficiency improvements, including removal of:
* redundant media loadings and ui updates
* frequent list search during audio play
* Various efficiency improvements
* streamed media somewhat equivalent to downloaded media
* enabled episode description on player detailed view
* enabled intro- and end- skipping
@ -77,6 +74,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Played or new episodes have clearer markings
* Sort dialog no longer dims the main view
* download date can be used to sort both feeds and episodes
* Subscriptions view has a filter based on feed preferences, in the same style as episodes filter
* Subscriptions sorting is now bi-directional based on various explicit measures
* in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward)
* Long-press filter button in FeedEpisode view enables/disables filters without changing filter settings
@ -106,8 +104,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Ability to open podcast from webpage address
* Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes
* Online feed episodes can be freely played (streamed) without a subscription
* externally shared feed opens in the new online feed view fragment
* OnlineFeedView` activity is stripped down to only receive externally shared feeds
* Youtube channels are accepted from external share or paste of address in podcast search view, and can be subscribed as a normal podcast, though video play is handled externally
### Instant (or Wifi) sync
@ -122,6 +118,8 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* When auto download is enabled in the Settings, feeds to be auto-downloaded need to be separately enabled in the feed settings.
* Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. "newest episodes" meaning most recent episodes, new or old)
* Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app.
* Auto downloads run feeds or feed refreshes, scheduled or manual
* auto download always includes any undownloaded episodes (regardless of feeds) added in the current queue
* After auto download run, episodes with New status is changed to Unplayed.
* auto download feed setting dialog is also changed:
* there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently

View File

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

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

View File

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

View File

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

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.
private var serviceStarterImpl = Runnable {}
fun needSynch() : Boolean {
return isProviderConnected
}
fun setServiceStarterImpl(serviceStarter: Runnable) {
serviceStarterImpl = serviceStarter
}
@ -57,9 +61,9 @@ object SynchronizationQueueSink {
fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) {
if (!isProviderConnected) return
if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return
val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY)
.currentTimestamp()
.started(media.startPosition / 1000)

View File

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

View File

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

View File

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

View File

@ -23,8 +23,9 @@ import java.util.*
import java.util.concurrent.ExecutionException
object AutoCleanups {
private val TAG: String = AutoCleanups::class.simpleName ?: "Anonymous"
var episodeCleanupValue: Int
private var episodeCleanupValue: Int
get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodeCleanup.name, "" + EPISODE_CLEANUP_NULL)!!.toInt()
set(episodeCleanupValue) {
appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply()
@ -102,9 +103,6 @@ object AutoCleanups {
}
return 0
}
companion object {
private val TAG: String = ExceptFavoriteCleanupAlgorithm::class.simpleName ?: "Anonymous"
}
}
/**
@ -154,9 +152,6 @@ object AutoCleanups {
public override fun getDefaultCleanupParameter(): Int {
return getNumEpisodesToCleanup(0)
}
companion object {
private val TAG: String = APQueueCleanupAlgorithm::class.simpleName ?: "Anonymous"
}
}
/**
@ -174,9 +169,6 @@ object AutoCleanups {
override fun getReclaimableItems(): Int {
return 0
}
companion object {
private val TAG: String = APNullCleanupAlgorithm::class.simpleName ?: "Anonymous"
}
}
/** the number of days after playback to wait before an item is eligible to be cleaned up.
@ -232,7 +224,6 @@ object AutoCleanups {
return getNumEpisodesToCleanup(0)
}
companion object {
private val TAG: String = APCleanupAlgorithm::class.simpleName ?: "Anonymous"
private fun minusHours(baseDate: Date, numberOfHours: Int): Date {
val cal = Calendar.getInstance()
cal.time = baseDate

View File

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

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

View File

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

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.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.content.Context
import android.util.Log
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
@ -29,6 +28,60 @@ object Queues {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
}
var isQueueLocked: Boolean
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, false)
set(locked) {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply()
}
var isQueueKeepSorted: Boolean
/**
* Returns if the queue is in keep sorted mode.
* @see .queueKeepSortedOrder
*/
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, false)
/**
* Enables/disables the keep sorted mode of the queue.
* @see .queueKeepSortedOrder
*/
set(keepSorted) {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, keepSorted).apply()
}
var queueKeepSortedOrder: EpisodeSortOrder?
/**
* Returns the sort order for the queue keep sorted mode.
* Note: This value is stored independently from the keep sorted state.
* @see .isQueueKeepSorted
*/
get() {
val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, "use-default")
return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD)
}
/**
* Sets the sort order for the queue keep sorted mode.
* @see .setQueueKeepSorted
*/
set(sortOrder) {
if (sortOrder == null) return
appPrefs.edit().putString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, sortOrder.name).apply()
}
var enqueueLocation: EnqueueLocation
get() {
val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name)
try {
return EnqueueLocation.valueOf(valStr!!)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)
return EnqueueLocation.BACK
}
}
set(location) {
appPrefs.edit().putString(UserPreferences.Prefs.prefEnqueueLocation.name, location.name).apply()
}
fun getInQueueEpisodeIds(): Set<Long> {
Logd(TAG, "getQueueIDList() called")
val queues = realm.query(PlayQueue::class).find()
@ -89,24 +142,25 @@ object Queues {
}
}
suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode) {
suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode, queue_: PlayQueue? = null) {
Logd(TAG, "addToQueueSync( ... ) called")
val queue = queue_ ?: curQueue
val currentlyPlaying = curMedia
val positionCalculator = EnqueuePositionCalculator(enqueueLocation)
var insertPosition = positionCalculator.calcPosition(curQueue.episodes, currentlyPlaying)
var insertPosition = positionCalculator.calcPosition(queue.episodes, currentlyPlaying)
if (curQueue.episodeIds.contains(episode.id)) return
if (queue.episodeIds.contains(episode.id)) return
curQueue.episodeIds.add(insertPosition, episode.id)
curQueue.episodes.add(insertPosition, episode)
queue.episodeIds.add(insertPosition, episode.id)
queue.episodes.add(insertPosition, episode)
insertPosition++
curQueue.update()
upsert(curQueue) {}
queue.update()
upsert(queue) {}
if (markAsUnplayed && episode.isNew) setPlayState(Episode.UNPLAYED, false, episode)
EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition))
if (queue_?.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition))
// if (performAutoDownload) autodownloadEpisodeMedia(context)
}
@ -147,13 +201,11 @@ object Queues {
/**
* Removes a Episode object from the queue.
* @param context A context that is used for opening a database connection.
* perform autodownloadEpisodeMedia only if context is not null
* @param episodes FeedItems that should be removed.
*/
@OptIn(UnstableApi::class) @JvmStatic
fun removeFromQueue(context: Context?, vararg episodes: Episode) : Job {
return runOnIOScope { removeFromQueueSync(curQueue, context, *episodes) }
fun removeFromQueue(vararg episodes: Episode) : Job {
return runOnIOScope { removeFromQueueSync(curQueue, *episodes) }
}
@OptIn(UnstableApi::class)
@ -161,22 +213,21 @@ object Queues {
Logd(TAG, "removeFromAllQueues called ")
val queues = realm.query(PlayQueue::class).find()
for (q in queues) {
if (q.id != curQueue.id) removeFromQueueSync(q, null, *episodes)
if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
}
// ensure curQueue is last updated
removeFromQueueSync(curQueue, null, *episodes)
removeFromQueueSync(curQueue, *episodes)
}
/**
* @param queue_ if null, use curQueue
* @param context perform autodownloadEpisodeMedia only if context is not null and queue_ is curQueue
*/
@UnstableApi
internal fun removeFromQueueSync(queue_: PlayQueue?, context: Context?, vararg episodes: Episode) {
internal fun removeFromQueueSync(queue_: PlayQueue?, vararg episodes: Episode) {
Logd(TAG, "removeFromQueueSync called ")
if (episodes.isEmpty()) return
val queue = queue_ ?: curQueue
var queue = queue_ ?: curQueue
val events: MutableList<FlowEvent.QueueEvent> = ArrayList()
val pos: MutableList<Int> = mutableListOf()
val qItems = queue.episodes.toMutableList()
@ -200,9 +251,6 @@ object Queues {
}
for (event in events) EventFlow.postEvent(event)
} else Logd(TAG, "Queue was not modified by call to removeQueueItem")
// TODO: what's this for?
// if (queue.id == curQueue.id && context != null) autodownloadEpisodeMedia(context)
}
suspend fun removeFromAllQueuesQuiet(episodeIds: List<Long>) {
@ -214,10 +262,11 @@ object Queues {
eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (eidsInQueues.isNotEmpty()) {
val qeids = q.episodeIds.minus(eidsInQueues)
q.episodeIds.clear()
q.episodeIds.addAll(qeids)
q.update()
upsert(q) {}
upsert(q) {
it.episodeIds.clear()
it.episodeIds.addAll(qeids)
it.update()
}
}
}
// ensure curQueue is last updated
@ -225,10 +274,11 @@ object Queues {
eidsInQueues = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (eidsInQueues.isNotEmpty()) {
val qeids = q.episodeIds.minus(eidsInQueues)
q.episodeIds.clear()
q.episodeIds.addAll(qeids)
q.update()
upsert(q) {}
upsert(q) {
it.episodeIds.clear()
it.episodeIds.addAll(qeids)
it.update()
}
}
}
@ -270,60 +320,6 @@ object Queues {
upsertBlk(curQueue) {}
}
var isQueueLocked: Boolean
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, false)
set(locked) {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply()
}
var isQueueKeepSorted: Boolean
/**
* Returns if the queue is in keep sorted mode.
* @see .queueKeepSortedOrder
*/
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, false)
/**
* Enables/disables the keep sorted mode of the queue.
* @see .queueKeepSortedOrder
*/
set(keepSorted) {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, keepSorted).apply()
}
var queueKeepSortedOrder: EpisodeSortOrder?
/**
* Returns the sort order for the queue keep sorted mode.
* Note: This value is stored independently from the keep sorted state.
* @see .isQueueKeepSorted
*/
get() {
val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, "use-default")
return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD)
}
/**
* Sets the sort order for the queue keep sorted mode.
* @see .setQueueKeepSorted
*/
set(sortOrder) {
if (sortOrder == null) return
appPrefs.edit().putString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, sortOrder.name).apply()
}
var enqueueLocation: EnqueueLocation
get() {
val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name)
try {
return EnqueueLocation.valueOf(valStr!!)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)
return EnqueueLocation.BACK
}
}
set(location) {
appPrefs.edit().putString(UserPreferences.Prefs.prefEnqueueLocation.name, location.name).apply()
}
class EnqueuePositionCalculator(private val enqueueLocation: EnqueueLocation) {
/**
* Determine the position (0-based) that the item(s) should be inserted to the named queue.

View File

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

View File

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

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
import ac.mdiq.podcini.ui.activity.MainActivity
import android.util.Log
import androidx.annotation.PluralsRes
import com.google.android.material.snackbar.Snackbar
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.databinding.SwitchQueueDialogBinding
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.PlayQueue
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.utils.LocalDeleteModal
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.app.Activity
import android.content.DialogInterface
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.annotation.PluralsRes
import androidx.media3.common.util.UnstableApi
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.runBlocking
import java.lang.ref.WeakReference
@UnstableApi
@ -26,6 +43,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
R.id.add_to_favorite_batch -> markFavorite(items, true)
R.id.remove_favorite_batch -> markFavorite(items, false)
R.id.add_to_queue_batch -> queueChecked(items)
R.id.put_to_queue_batch -> putToQueue(items)
R.id.remove_from_queue_batch -> removeFromQueueChecked(items)
R.id.mark_read_batch -> {
setPlayState(Episode.PLAYED, false, *items.toTypedArray())
@ -53,7 +71,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
private fun removeFromQueueChecked(items: List<Episode>) {
val checkedIds = getSelectedIds(items)
removeFromQueue(activity, *items.toTypedArray())
removeFromQueue(*items.toTypedArray())
showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.size)
}
@ -96,6 +114,59 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
return checkedIds
}
private fun putToQueue(items: List<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 {
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.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch
import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.model.Episode
@ -145,7 +145,7 @@ object EpisodeMenuHandler {
if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) {
val media: EpisodeMedia? = selectedItem.media
// not all items have media, Gpodder only cares about those that do
if (media != null) {
if (needSynch() && media != null) {
val actionPlay: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY)
.currentTimestamp()
.started(media.getDuration() / 1000)
@ -159,7 +159,7 @@ object EpisodeMenuHandler {
R.id.mark_unread_item -> {
selectedItem.setPlayed(false)
setPlayState(Episode.UNPLAYED, false, selectedItem)
if (selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) {
if (needSynch() && selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) {
val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp()
.build()
@ -167,7 +167,7 @@ object EpisodeMenuHandler {
}
}
R.id.add_to_queue_item -> addToQueue(true, selectedItem)
R.id.remove_from_queue_item -> removeFromQueue(context, selectedItem)
R.id.remove_from_queue_item -> removeFromQueue(selectedItem)
R.id.add_to_favorites_item -> setFavorite(selectedItem, true)
R.id.remove_from_favorites_item -> setFavorite(selectedItem, false)
R.id.reset_position -> {

View File

@ -37,7 +37,7 @@ class RemoveFromQueueSwipeAction : SwipeAction {
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
val position: Int = curQueue.episodes.indexOf(item)
removeFromQueue(fragment.requireActivity(), item)
removeFromQueue(item)
if (willRemove(filter, item)) {
(fragment.requireActivity() as MainActivity).showSnackbarAbovePlayer(fragment.resources.getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), Snackbar.LENGTH_LONG)
.setAction(fragment.getString(R.string.undo)) {

View File

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

View File

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

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
if (currentMedia?.getIdentifier() == null || media?.getIdentifier() != currentMedia?.getIdentifier()) {
currentMedia = media
playerUI?.updateUi(currentMedia)
playerDetailsFragment?.setItem(curEpisode!!)
}
playerUI?.onPositionUpdate(event)

View File

@ -131,6 +131,7 @@ class FeedSettingsFragment : Fragment() {
override fun onDestroyView() {
Logd(TAG, "onDestroyView")
feed = null
feedPrefs = null
super.onDestroyView()
}
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.unmanaged
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
@ -49,7 +47,10 @@ import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.Spinner
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
@ -111,6 +112,23 @@ import java.util.*
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
val queues = realm.query(PlayQueue::class).find()
val queueNames = queues.map { it.name }.toTypedArray()
val spinnerLayout = inflater.inflate(R.layout.queue_title_spinner, null)
val spinner = spinnerLayout.findViewById<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)
toolbar.inflateMenu(R.menu.queue)
refreshToolbarState()
@ -324,7 +342,7 @@ import java.util.*
for (downloadUrl in event.urls) {
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), downloadUrl)
if (pos >= 0) {
val item = queueItems[pos]
val item = unmanaged(queueItems[pos])
// item.media?.downloaded = true
item.media?.setIsDownloaded()
adapter?.notifyItemChangedCompat(pos)
@ -535,7 +553,7 @@ import java.util.*
info += DurationConverter.getDurationStringLocalized(requireActivity(), timeLeft)
}
binding.infoBar.text = info
toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}"
// toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}"
}
private var loadItemsRunning = false

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

View File

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

View File

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

View File

@ -43,6 +43,24 @@
android:layout_height="match_parent"
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
android:id="@+id/count"
android:layout_width="wrap_content"

View File

@ -19,8 +19,7 @@
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator"
app:title="@string/queue_label" />
app:navigationIcon="?homeAsUpIndicator"/>
<LinearLayout
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: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
android:id="@+id/add_to_favorite_batch"
android:icon="@drawable/ic_star"

View File

@ -6,6 +6,12 @@
android:icon="@drawable/ic_search"
custom:showAsAction="always"
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
android:id="@+id/subscriptions_sort"
android:title="@string/sort"

View File

@ -20,8 +20,8 @@
<dimen name="listitem_iconwithtext_textleftpadding">16dp</dimen>
<dimen name="listitem_threeline_textleftpadding">16dp</dimen>
<dimen name="listitem_threeline_textrightpadding">8dp</dimen>
<dimen name="listitem_threeline_verticalpadding">8dp</dimen>
<dimen name="listitem_threeline_textrightpadding">6dp</dimen>
<dimen name="listitem_threeline_verticalpadding">5dp</dimen>
<dimen name="list_vertical_padding">8dp</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_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="episode_cleanup_never">Never</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="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 -->
<string name="share_playback_position_dialog_label">Include playback position</string>
<string name="share_dialog_episode_website_label">Episode webpage</string>

View File

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

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
* fixed player UI not updating on change of episode
* fixed the mal-function of restoring previously backed-up OPML
* reduced reactions to PlaybackPositionEvent
* tuned AutoCleanup a bit
* tuned and fixed some some issues in audo-downloaded
* tuned and fixed some some issues in auto-downloaded
# 6.1.0
@ -17,7 +26,7 @@
* skipped concurrent calls for loading data in multiple views
* toggle "Auto backup of OPML" in Settings will restart Podcini
* automatically restoring backup of OPML upon new install is disabled. Instead, in AddFeed view, when subscription is empty and OPML backup is available, a dialog is shown to ask about restoring.
* added audo downloadable to episodes filter
* added auto downloadable to episodes filter
* added download date to episodes sorting
* added download date to feed sorting
* auto download algorithm is changed to individual feed based.

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