6.1.2 commit
This commit is contained in:
parent
d47bdab971
commit
3c2618a29a
|
@ -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
|
||||||
|
|
10
README.md
10
README.md
|
@ -38,7 +38,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
|
||||||
|
|
||||||
* More convenient player control displayed on all pages
|
* 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
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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!!)
|
||||||
|
|
|
@ -43,6 +43,8 @@ object UserPreferences {
|
||||||
prefDefaultPage,
|
prefDefaultPage,
|
||||||
prefBackButtonOpensDrawer,
|
prefBackButtonOpensDrawer,
|
||||||
|
|
||||||
|
prefFeedFilter,
|
||||||
|
|
||||||
prefQueueKeepSorted,
|
prefQueueKeepSorted,
|
||||||
prefQueueKeepSortedOrder,
|
prefQueueKeepSortedOrder,
|
||||||
prefDownloadSortedOrder,
|
prefDownloadSortedOrder,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
13
changelog.md
13
changelog.md
|
@ -1,10 +1,19 @@
|
||||||
|
# 6.1.2
|
||||||
|
|
||||||
|
* fixed crash issue when setting the inclusive or exclusive filters in feed auto-download setting
|
||||||
|
* fixed player UI not updating on change of episode
|
||||||
|
* changed title of Queues view to a spinner for easily switching queues
|
||||||
|
* added "Put to queue" in multi-select menu putting selected episodes to a queue, this would also remove the episodes from any previous queues.
|
||||||
|
* added condition checks for preparing enqueuing sync actions
|
||||||
|
* in Subscriptions view added feeds filter based on feed preferences, in the same style as episodes filter
|
||||||
|
|
||||||
# 6.1.1
|
# 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.
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue