6.1.3 commit

This commit is contained in:
Xilin Jia 2024-07-22 13:03:04 +01:00
parent 3c2618a29a
commit 742aa3615d
31 changed files with 371 additions and 327 deletions

View File

@ -75,10 +75,11 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Sort dialog no longer dims the main view
* download date can be used to sort both feeds and episodes
* Subscriptions view has a filter based on feed preferences, in the same style as episodes filter
* Subscriptions sorting is now bi-directional based on various explicit measures
* Subscriptions sorting is now bi-directional based on various explicit measures, and sorting info is shown on every feed (List Layout only)
* in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view)
* in all episodes list views, click on an episode image brings up the FeedInfo view
* 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
* in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view)
* History view shows time of last play, and allows filters and sorts
* Multiple queues can be used: 5 queues are provided by default: Default queue, and Queues 1-4
* all queue operations are on the curQueue, which can be set in all episodes list views
@ -90,8 +91,9 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
### Podcast/Episode
* New share notes menu option on various episode views
* Feed info view offers a link for direct search of feeds related to author
* FeedInfo view offers a link for direct search of feeds related to author
* FeedInfo view has button showing number of episodes to open the FeedEpisodes view
* FeedInfo view has feed setting in the header
* in EpisodeInfo view, "mark played/unplayed", "add to/remove from queue", and "favoraite/unfovorite" are at the action bar
* New episode home view with two display modes: webpage or reader
* In episode, in addition to "description" there is a new "transcript" field to save text (if any) fetched from the episode's website

View File

@ -126,8 +126,8 @@ android {
buildConfig true
}
defaultConfig {
versionCode 3020216
versionName "6.1.2"
versionCode 3020217
versionName "6.1.3"
applicationId "ac.mdiq.podcini.R"
def commit = ""
@ -245,6 +245,8 @@ dependencies {
implementation "net.dankito.readability4j:readability4j:1.0.8"
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
// Non-free dependencies:
playImplementation 'com.google.android.play:core-ktx:1.8.1'
compileOnly "com.google.android.wearable:wearable:2.9.0"

View File

@ -332,11 +332,10 @@ object FeedUpdateManager {
if (isSuccessful) {
downloadStatus = DownloadResult(feed.id, feed.getTextIdentifier()?:"", DownloadError.SUCCESS, isSuccessful, reasonDetailed?:"")
return result
} else {
}
downloadStatus = DownloadResult(feed.id, feed.getTextIdentifier()?:"", reason?: DownloadError.ERROR_NOT_FOUND, isSuccessful, reasonDetailed?:"")
return null
}
}
/**
* Checks if the feed was parsed correctly.
*/

View File

@ -8,7 +8,6 @@ import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.database.RealmDB.realm
@ -73,46 +72,46 @@ object AutoDownloads {
@UnstableApi
open fun autoDownloadEpisodeMedia(context: Context, feeds: List<Feed>? = null): Runnable? {
return Runnable {
// true if we should auto download based on network status
// val networkShouldAutoDl = (isAutoDownloadAllowed)
val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload)
// true if we should auto download based on power status
val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery)
Logd(TAG, "prepare autoDownloadUndownloadedItems $networkShouldAutoDl $powerShouldAutoDl")
// we should only auto download if both network AND power are happy
if (networkShouldAutoDl && powerShouldAutoDl) {
Logd(TAG, "Performing auto-dl of undownloaded episodes")
val queueItems = curQueue.episodes
val newItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.new.name), EpisodeSortOrder.DATE_NEW_OLD)
Logd(TAG, "newItems: ${newItems.size}")
val candidates: MutableList<Episode> = ArrayList(queueItems.size + newItems.size)
candidates.addAll(queueItems)
for (newItem in newItems) {
val feedPrefs = newItem.feed!!.preferences
if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.autoDownloadFilter!!.shouldAutoDownload(newItem)) candidates.add(newItem)
}
// filter items that are not auto downloadable
val it = candidates.iterator()
while (it.hasNext()) {
val item = it.next()
if (!item.isAutoDownloadEnabled || item.isDownloaded || item.media == null || isCurMedia(item.media) || item.feed?.isLocalFeed == true)
it.remove()
}
val autoDownloadableEpisodes = candidates.size
val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
val deletedEpisodes = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableEpisodes)
val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
val episodeCacheSize = episodeCacheSize
val episodeSpaceLeft =
if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) autoDownloadableEpisodes
else episodeCacheSize - (downloadedEpisodes - deletedEpisodes)
val itemsToDownload: List<Episode> = candidates.subList(0, episodeSpaceLeft)
if (itemsToDownload.isNotEmpty()) {
Logd(TAG, "Enqueueing " + itemsToDownload.size + " items for download")
for (episode in itemsToDownload) DownloadServiceInterface.get()?.download(context, episode)
}
}
else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl")
// // true if we should auto download based on network status
//// val networkShouldAutoDl = (isAutoDownloadAllowed)
// val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload)
// // true if we should auto download based on power status
// val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery)
// Logd(TAG, "prepare autoDownloadUndownloadedItems $networkShouldAutoDl $powerShouldAutoDl")
// // we should only auto download if both network AND power are happy
// if (networkShouldAutoDl && powerShouldAutoDl) {
// Logd(TAG, "Performing auto-dl of undownloaded episodes")
// val queueItems = curQueue.episodes
// val newItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.new.name), EpisodeSortOrder.DATE_NEW_OLD)
// Logd(TAG, "newItems: ${newItems.size}")
// val candidates: MutableList<Episode> = ArrayList(queueItems.size + newItems.size)
// candidates.addAll(queueItems)
// for (newItem in newItems) {
// val feedPrefs = newItem.feed!!.preferences
// if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.autoDownloadFilter!!.shouldAutoDownload(newItem)) candidates.add(newItem)
// }
// // filter items that are not auto downloadable
// val it = candidates.iterator()
// while (it.hasNext()) {
// val item = it.next()
// if (!item.isAutoDownloadEnabled || item.isDownloaded || item.media == null || isCurMedia(item.media) || item.feed?.isLocalFeed == true)
// it.remove()
// }
// val autoDownloadableEpisodes = candidates.size
// val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
// val deletedEpisodes = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableEpisodes)
// val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
// val episodeCacheSize = episodeCacheSize
// val episodeSpaceLeft =
// if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) autoDownloadableEpisodes
// else episodeCacheSize - (downloadedEpisodes - deletedEpisodes)
// val itemsToDownload: List<Episode> = candidates.subList(0, episodeSpaceLeft)
// if (itemsToDownload.isNotEmpty()) {
// Logd(TAG, "Enqueueing " + itemsToDownload.size + " items for download")
// for (episode in itemsToDownload) DownloadServiceInterface.get()?.download(context, episode)
// }
// }
// else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl")
}
}
@ -150,7 +149,10 @@ object AutoDownloads {
feeds.forEach { f ->
if (f.preferences?.autoDownload == true && !f.isLocalFeed) {
var episodes = mutableListOf<Episode>()
val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name), f.id)
val dlFilter =
if (f.preferences?.countingPlayed == true) EpisodeFilter(EpisodeFilter.States.downloaded.name)
else EpisodeFilter(EpisodeFilter.States.downloaded.name, EpisodeFilter.States.unplayed.name)
val downloadedCount = getEpisodesCount(dlFilter, f.id)
val allowedDLCount = (f.preferences?.autoDLMaxEpisodes?:0) - downloadedCount
Logd(TAG, "autoDownloadEpisodeMedia ${f.preferences?.autoDLMaxEpisodes} downloadedCount: $downloadedCount allowedDLCount: $allowedDLCount")
if (allowedDLCount > 0) {
@ -198,7 +200,7 @@ object AutoDownloads {
}
}
}
// TODO: need to send an event
// TODO: probably need to send an event
}
}
if (candidates.isNotEmpty()) {

View File

@ -36,17 +36,13 @@ import kotlin.math.abs
object Feeds {
private val TAG: String = Feeds::class.simpleName ?: "Anonymous"
private val feedMap: MutableMap<Long, Feed> = mutableMapOf()
private val tags: MutableList<String> = mutableListOf()
@Synchronized
fun getFeedList(queryString: String = "", fromDB: Boolean = true): List<Feed> {
if (fromDB) {
fun getFeedList(queryString: String = ""): List<Feed> {
return if (queryString.isEmpty()) realm.query(Feed::class).find()
else realm.query(Feed::class, queryString).find()
}
return feedMap.values.toList()
}
fun getFeedCount(): Int {
return realm.query(Feed::class).count().find().toInt()
@ -56,26 +52,6 @@ object Feeds {
return tags
}
@Synchronized
fun updateFeedMap(feeds: List<Feed> = listOf(), wipe: Boolean = false) {
Logd(TAG, "updateFeedMap called feeds: ${feeds.size} wipe: $wipe")
when {
feeds.isEmpty() -> {
val feeds_ = realm.query(Feed::class).find()
feedMap.clear()
feedMap.putAll(feeds_.associateBy { it.id })
}
wipe -> {
feedMap.clear()
feedMap.putAll(feeds.associateBy { it.id })
}
else -> {
for (f in feeds) feedMap[f.id] = f
}
}
buildTags()
}
fun buildTags() {
val tagsSet = mutableSetOf<String>()
val feedsCopy = getFeedList()
@ -174,13 +150,13 @@ object Feeds {
return result
}
fun getFeed(feedId: Long, copy: Boolean = false, fromDB: Boolean = true): Feed? {
fun getFeed(feedId: Long, copy: Boolean = false): Feed? {
if (BuildConfig.DEBUG) {
val stackTrace = Thread.currentThread().stackTrace
val caller = if (stackTrace.size > 3) stackTrace[3] else null
Logd(TAG, "${caller?.className}.${caller?.methodName} getFeed called fromDB: $fromDB")
Logd(TAG, "${caller?.className}.${caller?.methodName} getFeed called")
}
val f = if (fromDB) realm.query(Feed::class, "id == $feedId").first().find() else feedMap[feedId]
val f = realm.query(Feed::class, "id == $feedId").first().find()
return if (f != null) {
if (copy) realm.copyFromRealm(f)
else f
@ -247,7 +223,6 @@ object Feeds {
// Look for new or updated Items
for (idx in newFeed.episodes.indices) {
val episode = newFeed.episodes[idx]
val possibleDuplicate = EpisodeAssistant.searchEpisodeGuessDuplicate(newFeed.episodes, episode)
if (!newFeed.isLocalFeed && possibleDuplicate != null && episode !== possibleDuplicate) {
// Canonical episode is the first one returned (usually oldest)
@ -263,7 +238,6 @@ object Feeds {
""".trimIndent()))
continue
}
var oldItem = EpisodeAssistant.searchEpisodeByIdentifyingValue(savedFeed.episodes, episode)
if (!newFeed.isLocalFeed && oldItem == null) {
oldItem = EpisodeAssistant.searchEpisodeGuessDuplicate(savedFeed.episodes, episode)
@ -394,7 +368,6 @@ object Feeds {
}
copyToRealm(feed)
}
// updateFeedMap(feeds.toList())
}
for (feed in feeds) {
if (!feed.isLocalFeed && feed.downloadUrl != null) SynchronizationQueueSink.enqueueFeedAddedIfSyncActive(context, feed.downloadUrl!!)
@ -451,7 +424,7 @@ object Feeds {
val feedToDelete = findLatest(feed_)
if (feedToDelete != null) {
delete(feedToDelete)
feedMap.remove(feedId)
// feedMap.remove(feedId)
}
}
}

View File

@ -155,12 +155,12 @@ object Queues {
queue.episodeIds.add(insertPosition, episode.id)
queue.episodes.add(insertPosition, episode)
insertPosition++
queue.update()
if (queue.id == curQueue.id) queue.update()
upsert(queue) {}
if (markAsUnplayed && episode.isNew) setPlayState(Episode.UNPLAYED, false, episode)
if (queue_?.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition))
if (queue.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition))
// if (performAutoDownload) autodownloadEpisodeMedia(context)
}

View File

@ -18,7 +18,7 @@ import kotlin.coroutines.ContinuationInterceptor
object RealmDB {
private val TAG: String = RealmDB::class.simpleName ?: "Anonymous"
private const val SCHEMA_VERSION_NUMBER = 10L
private const val SCHEMA_VERSION_NUMBER = 11L
private val ioScope = CoroutineScope(Dispatchers.IO)

View File

@ -51,7 +51,7 @@ class Episode : RealmObject {
@Ignore
var feed: Feed? = null
get() {
if (field == null && feedId != null) field = getFeed(feedId!!, fromDB = true)
if (field == null && feedId != null) field = getFeed(feedId!!)
return field
}

View File

@ -1,5 +1,6 @@
package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.FeedFunding.Companion.extractPaymentLinks
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
import io.realm.kotlin.ext.realmListOf
@ -132,29 +133,30 @@ class Feed : RealmObject {
preferences?.sortOrderCode = value.code
}
@Ignore
var sortOrderAux: EpisodeSortOrder? = null
get() = fromCode(preferences?.sortOrderAuxCode ?: 0)
set(value) {
if (value == null) return
field = value
preferences?.sortOrderAuxCode = value.code
}
// @Ignore
// var sortOrderAux: EpisodeSortOrder? = null
// get() = fromCode(preferences?.sortOrderAuxCode ?: 0)
// set(value) {
// if (value == null) return
// field = value
// preferences?.sortOrderAuxCode = value.code
// }
@Ignore
val mostRecentItem: Episode?
get() {
// we could sort, but we don't need to, a simple search is fine...
var mostRecentDate = Date(0)
var mostRecentItem: Episode? = null
for (item in episodes) {
val date = item.getPubDate()
if (date != null && date.after(mostRecentDate)) {
mostRecentDate = date
mostRecentItem = item
}
}
return mostRecentItem
// // we could sort, but we don't need to, a simple search is fine...
// var mostRecentDate = Date(0)
// var mostRecentItem: Episode? = null
// for (item in episodes) {
// val date = item.getPubDate()
// if (date != null && date.after(mostRecentDate)) {
// mostRecentDate = date
// mostRecentItem = item
// }
// }
// return mostRecentItem
return realm.query(Episode::class).query("feedId == $id SORT(pubDate DESC)").first().find()
}
@Ignore
@ -164,6 +166,9 @@ class Feed : RealmObject {
this.eigenTitle = value
}
@Ignore
var sortInfo: String = ""
/**
* This constructor is used for test purposes.
*/

View File

@ -45,6 +45,13 @@ class FeedPreferences : EmbeddedRealmObject {
}
var volumeAdaption: Int = 0
var filterString: String = ""
var sortOrderCode: Int = 0 // in EpisodeSortOrder
// seems not too useful
// var sortOrderAuxCode: Int = 0 // in EpisodeSortOrder
@Ignore
val tagsAsString: String
get() = tags.joinToString(TAG_SEPARATOR)
@ -69,6 +76,8 @@ class FeedPreferences : EmbeddedRealmObject {
var autoDLMaxEpisodes: Int = 3
var countingPlayed: Boolean = true
@Ignore
var autoDLPolicy: AutoDLPolicy = AutoDLPolicy.ONLY_NEW
get() = AutoDLPolicy.fromCode(autoDLPolicyCode)
@ -78,12 +87,6 @@ class FeedPreferences : EmbeddedRealmObject {
}
var autoDLPolicyCode: Int = 0
var filterString: String = ""
var sortOrderCode: Int = 0
var sortOrderAuxCode: Int = 0
enum class AutoDLPolicy(val code: Int) {
ONLY_NEW(0),
NEWER(1),

View File

@ -19,39 +19,23 @@ object EpisodesPermutors {
var permutor: Permutor<Episode>? = null
when (sortOrder) {
EpisodeSortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(
itemTitle(f2)) }
EpisodeSortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(
itemTitle(f1)) }
EpisodeSortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(
pubDate(f2)) }
EpisodeSortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(
pubDate(f1)) }
EpisodeSortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(
duration(f2)) }
EpisodeSortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(
duration(f1)) }
EpisodeSortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(
itemLink(f2)) }
EpisodeSortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(
itemLink(f1)) }
EpisodeSortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(
playDate(f2)) }
EpisodeSortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(
playDate(f1)) }
EpisodeSortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(
completeDate(f2)) }
EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(
completeDate(f1)) }
EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(
downloadDate(f2)) }
EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(
downloadDate(f1)) }
EpisodeSortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
EpisodeSortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
EpisodeSortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
EpisodeSortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
EpisodeSortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
EpisodeSortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
EpisodeSortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
EpisodeSortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
EpisodeSortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
EpisodeSortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
EpisodeSortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
EpisodeSortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(
feedTitle(f2)) }
EpisodeSortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(
feedTitle(f1)) }
EpisodeSortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
EpisodeSortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
EpisodeSortOrder.RANDOM -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) queue.shuffle()
@ -67,10 +51,8 @@ object EpisodesPermutors {
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, false)
}
}
EpisodeSortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(
size(f2)) }
EpisodeSortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(
size(f1)) }
EpisodeSortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
EpisodeSortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
}
if (comparator != null) {
val comparator2: Comparator<Episode> = comparator

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.ui.actions
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SwitchQueueDialogBinding
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.storage.database.Episodes
@ -22,9 +22,7 @@ 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 android.widget.RadioButton
import androidx.annotation.PluralsRes
import androidx.media3.common.util.UnstableApi
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -43,7 +41,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
R.id.add_to_favorite_batch -> markFavorite(items, true)
R.id.remove_favorite_batch -> markFavorite(items, false)
R.id.add_to_queue_batch -> queueChecked(items)
R.id.put_to_queue_batch -> putToQueue(items)
R.id.put_in_queue_batch -> PutToQueueDialog(activity, items).show()
R.id.remove_from_queue_batch -> removeFromQueueChecked(items)
R.id.mark_read_batch -> {
setPlayState(Episode.PLAYED, false, *items.toTypedArray())
@ -114,33 +112,29 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
return checkedIds
}
private fun putToQueue(items: List<Episode>) {
PutToQueueDialog(activity as MainActivity, items).show()
}
class PutToQueueDialog(activity: Activity, val items: List<Episode>) {
private val activityRef: WeakReference<Activity> = WeakReference(activity)
fun show() {
val activity = activityRef.get() ?: return
val binding = SwitchQueueDialogBinding.inflate(LayoutInflater.from(activity))
val binding = SelectQueueDialogBinding.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])
for (i in queues.indices) {
val radioButton = RadioButton(activity)
radioButton.text = queues[i].name
radioButton.textSize = 20f
radioButton.tag = i
binding.radioGroup.addView(radioButton)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
var toQueue: PlayQueue = curQueue
binding.radioGroup.setOnCheckedChangeListener { group, checkedId ->
val radioButton = group.findViewById<RadioButton>(checkedId)
val selectedIndex = radioButton.tag as Int
toQueue = unmanaged(queues[selectedIndex])
}
MaterialAlertDialogBuilder(activity)
.setView(binding.root)
.setTitle(R.string.switch_queue_label)
.setTitle(R.string.put_in_queue_label)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val queues = realm.query(PlayQueue::class).find()
val toRemove = mutableSetOf<Long>()

View File

@ -7,6 +7,7 @@ import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder
import android.R.color
@ -78,7 +79,8 @@ open class EpisodesAdapter(mainActivity: MainActivity)
return EpisodeViewHolder(mainActivityRef.get()!!, parent)
}
@UnstableApi override fun onBindViewHolder(holder: EpisodeViewHolder, pos: Int) {
@UnstableApi
override fun onBindViewHolder(holder: EpisodeViewHolder, pos: Int) {
if (pos >= episodes.size || pos < 0) {
beforeBindViewHolder(holder, pos)
holder.bindDummy()
@ -112,7 +114,7 @@ open class EpisodesAdapter(mainActivity: MainActivity)
}
holder.coverHolder.setOnClickListener {
val activity: MainActivity? = mainActivityRef.get()
if (!inActionMode()) activity?.loadChildFragment(EpisodeInfoFragment.newInstance(episodes[pos]))
if (!inActionMode() && episodes[pos].feed != null) activity?.loadChildFragment(FeedInfoFragment.newInstance(episodes[pos].feed!!))
else toggleSelection(holder.bindingAdapterPosition)
}
holder.itemView.setOnTouchListener(View.OnTouchListener { _: View?, e: MotionEvent ->
@ -149,6 +151,11 @@ open class EpisodesAdapter(mainActivity: MainActivity)
protected open fun afterBindViewHolder(holder: EpisodeViewHolder, pos: Int) {}
override fun onViewDetachedFromWindow(holder: EpisodeViewHolder) {
super.onViewDetachedFromWindow(holder)
// visibleItemsPositions.remove(holder.adapterPosition)
}
@UnstableApi override fun onViewRecycled(holder: EpisodeViewHolder) {
super.onViewRecycled(holder)
// Set all listeners to null. This is required to prevent leaking fragments that have set a listener.
@ -160,6 +167,7 @@ open class EpisodesAdapter(mainActivity: MainActivity)
holder.secondaryActionButton.setOnClickListener(null)
holder.dragHandle.setOnTouchListener(null)
holder.coverHolder.setOnTouchListener(null)
holder.episode = null
}
/**

View File

@ -1,9 +1,10 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SwitchQueueDialogBinding
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.PlayQueue
@ -11,10 +12,9 @@ import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.app.Activity
import android.content.DialogInterface
import android.os.Debug
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.RadioButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
@ -23,33 +23,37 @@ class SwitchQueueDialog(activity: Activity) {
fun show() {
val activity = activityRef.get() ?: return
val binding = SwitchQueueDialogBinding.inflate(LayoutInflater.from(activity))
val binding = SelectQueueDialogBinding.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 curQueue_: PlayQueue = curQueue
catSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
curQueue_ = queues[position]
for (i in queues.indices) {
val radioButton = RadioButton(activity)
radioButton.text = queues[i].name
radioButton.textSize = 20f
radioButton.tag = i
binding.radioGroup.addView(radioButton)
if (queues[i].id == curQueue.id) binding.radioGroup.check(radioButton.id)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
binding.radioGroup.setOnCheckedChangeListener { group, checkedId ->
binding.radioGroup.check(checkedId)
val radioButton = group.findViewById<RadioButton>(checkedId)
val selectedIndex = radioButton.tag as Int
curQueue_ = queues[selectedIndex]
}
MaterialAlertDialogBuilder(activity)
.setView(binding.root)
.setTitle(R.string.switch_queue_label)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
if (curQueue_.id != curQueue.id) {
val items = mutableListOf<Episode>()
items.addAll(curQueue.episodes)
items.addAll(curQueue_.episodes)
curQueue = realm.copyFromRealm(curQueue_)
curQueue = unmanaged(curQueue_)
curQueue.update()
upsertBlk(curQueue) {}
EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items))
}
}
.setNegativeButton(R.string.cancel_label, null)
.show()
}

View File

@ -245,7 +245,9 @@ import java.util.*
val item = event.episode
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes[pos] = item
episodes[pos] = unmanaged(episodes[pos])
episodes[pos].isFavorite = item.isFavorite
// episodes[pos] = item
adapter.notifyItemChangedCompat(pos)
}
}
@ -283,7 +285,7 @@ import java.util.*
var i = 0
val size: Int = event.episodes.size
while (i < size) {
val item: Episode = event.episodes[i]
val item: Episode = event.episodes[i++]
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes.removeAt(pos)
@ -295,7 +297,6 @@ import java.util.*
// adapter.notifyItemRemoved(pos)
}
}
i++
}
// have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash
if (size > 0) {

View File

@ -336,7 +336,7 @@ import java.util.concurrent.Semaphore
var i = 0
val size: Int = event.episodes.size
while (i < size) {
val item = event.episodes[i]
val item = event.episodes[i++]
if (item.feedId != feed!!.id) continue
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
@ -344,7 +344,6 @@ import java.util.concurrent.Semaphore
episodes[pos] = item
adapter.notifyItemChangedCompat(pos)
}
i++
}
}
@ -353,16 +352,10 @@ import java.util.concurrent.Semaphore
var i = 0
val size: Int = event.episodes.size
while (i < size) {
val item = event.episodes[i]
val item = event.episodes[i++]
if (item.feedId != feed!!.id) continue
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
// episodes[pos] = item
adapter.notifyItemChangedCompat(pos)
// episodes[pos].playState = item.playState
// adapter.notifyItemChangedCompat(pos)
}
i++
adapter.notifyDataSetChanged()
break
}
}
@ -396,7 +389,9 @@ import java.util.concurrent.Semaphore
val item = event.episode
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes[pos] = item
episodes[pos] = unmanaged(episodes[pos])
episodes[pos].isFavorite = item.isFavorite
// episodes[pos] = item
adapter.notifyItemChangedCompat(pos)
}
}
@ -626,7 +621,7 @@ import java.util.concurrent.Semaphore
lifecycleScope.launch {
try {
feed = withContext(Dispatchers.IO) {
val feed_ = getFeed(feedID, fromDB = true)
val feed_ = getFeed(feedID)
if (feed_ != null) {
Logd(TAG, "loadItems feed_.episodes.size: ${feed_.episodes.size}")
episodes.clear()

View File

@ -116,6 +116,10 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
(activity as MainActivity).loadChildFragment(fragment)
}
binding.header.butShowSettings.setOnClickListener {
val fragment = FeedSettingsFragment.newInstance(feed)
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
}
binding.btnvRelatedFeeds.setOnClickListener {
val fragment = OnlineSearchFragment.newInstance(CombinedSearcher::class.java, "${binding.header.txtvAuthor.text} podcasts")

View File

@ -112,8 +112,8 @@ class FeedSettingsFragment : Fragment() {
setupAuthentificationPreference()
updateAutoDownloadPolicy()
setupAutoDownloadPolicy()
updateAutoDownloadCacheSize()
setupAutoDownloadCacheSize()
setupCountingPlayedPreference()
setupAutoDownloadFilterPreference()
setupPlaybackSpeedPreference()
setupFeedAutoSkipPreference()
@ -203,20 +203,30 @@ class FeedSettingsFragment : Fragment() {
}
}
@UnstableApi private fun setupAutoDownloadCacheSize() {
val cachePref = findPreference<Preference>(Prefs.feedEpisodeCacheSize.name)
cachePref!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
val cachePref = findPreference<ListPreference>(Prefs.feedEpisodeCacheSize.name)
cachePref!!.value = feedPrefs!!.autoDLMaxEpisodes.toString()
cachePref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
if (feedPrefs != null) {
feedPrefs!!.autoDLMaxEpisodes = newValue.toString().toInt()
cachePref.value = feedPrefs!!.autoDLMaxEpisodes.toString()
persistFeedPreferences(feed!!)
updateAutoDownloadCacheSize()
}
false
}
}
private fun updateAutoDownloadCacheSize() {
@OptIn(UnstableApi::class) private fun setupCountingPlayedPreference() {
if (feedPrefs == null) return
val cachePref = findPreference<ListPreference>(Prefs.feedEpisodeCacheSize.name)
cachePref!!.value = feedPrefs!!.autoDLMaxEpisodes.toString()
val pref = findPreference<SwitchPreferenceCompat>(Prefs.countingPlayed.name)
pref!!.isChecked = feedPrefs!!.countingPlayed
pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val checked = newValue == true
if (feedPrefs != null) {
feedPrefs!!.countingPlayed = checked
persistFeedPreferences(feed!!)
}
pref.isChecked = checked
false
}
}
private fun setupAutoDownloadFilterPreference() {
findPreference<Preference>(Prefs.episodeInclusiveFilter.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
@ -332,7 +342,7 @@ class FeedSettingsFragment : Fragment() {
}
@OptIn(UnstableApi::class) private fun setupKeepUpdatedPreference() {
if (feedPrefs == null) return
val pref = findPreference<SwitchPreferenceCompat>("keepUpdated")
val pref = findPreference<SwitchPreferenceCompat>(Prefs.keepUpdated.name)
pref!!.isChecked = feedPrefs!!.keepUpdated
pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
val checked = newValue == true
@ -351,9 +361,10 @@ class FeedSettingsFragment : Fragment() {
autodl.isEnabled = false
autodl.setSummary(R.string.auto_download_disabled_globally)
findPreference<Preference>(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = false
findPreference<Preference>(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = false
findPreference<Preference>(Prefs.countingPlayed.name)!!.isEnabled = false
findPreference<Preference>(Prefs.episodeInclusiveFilter.name)!!.isEnabled = false
findPreference<Preference>(Prefs.episodeExclusiveFilter.name)!!.isEnabled = false
findPreference<Preference>(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = false
}
}
@OptIn(UnstableApi::class) private fun setupAutoDownloadPreference() {
@ -380,9 +391,10 @@ class FeedSettingsFragment : Fragment() {
if (feed?.preferences != null) {
val enabled = feed!!.preferences!!.autoDownload && isEnableAutodownload
findPreference<Preference>(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = enabled
findPreference<Preference>(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = enabled
findPreference<Preference>(Prefs.countingPlayed.name)!!.isEnabled = enabled
findPreference<Preference>(Prefs.episodeInclusiveFilter.name)!!.isEnabled = enabled
findPreference<Preference>(Prefs.episodeExclusiveFilter.name)!!.isEnabled = enabled
findPreference<Preference>(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = enabled
}
}
private fun setupTags() {
@ -397,6 +409,7 @@ class FeedSettingsFragment : Fragment() {
private enum class Prefs {
feedSettingsScreen,
keepUpdated,
authentication,
autoDelete,
feedPlaybackSpeed,
@ -404,10 +417,11 @@ class FeedSettingsFragment : Fragment() {
tags,
autoDownloadCategory,
autoDownload,
feedAutoDownloadPolicy,
feedEpisodeCacheSize,
countingPlayed,
episodeInclusiveFilter,
episodeExclusiveFilter,
feedEpisodeCacheSize,
feedAutoDownloadPolicy
}
companion object {

View File

@ -306,6 +306,7 @@ import java.util.*
val item = event.episode
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, item.id)
if (pos >= 0) {
queueItems[pos] = unmanaged(queueItems[pos])
queueItems[pos].isFavorite = item.isFavorite
adapter?.notifyItemChangedCompat(pos)
}
@ -320,14 +321,13 @@ import java.util.*
var i = 0
val size: Int = event.episodes.size
while (i < size) {
val item: Episode = event.episodes[i]
val item: Episode = event.episodes[i++]
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, item.id)
if (pos >= 0) {
queueItems[pos] = item
adapter?.notifyItemChangedCompat(pos)
refreshInfoBar()
}
i++
}
}
@ -418,6 +418,7 @@ import java.util.*
val keepSorted: Boolean = isQueueKeepSorted
toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked)
toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted)
toolbar.menu.findItem(R.id.switch_queue).setVisible(false)
}
@UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean {
@ -437,7 +438,7 @@ import java.util.*
conDialog.createNewDialog().show()
}
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
else -> return false
}
return true

View File

@ -278,13 +278,12 @@ import java.lang.ref.WeakReference
var i = 0
val size: Int = event.episodes.size
while (i < size) {
val item: Episode = event.episodes[i]
val item: Episode = event.episodes[i++]
val pos: Int = EpisodeUtil.indexOfItemWithId(results, item.id)
if (pos >= 0) {
results[pos] = item
adapter.notifyItemChangedCompat(pos)
}
i++
}
}

View File

@ -14,8 +14,10 @@ import ac.mdiq.podcini.ui.actions.menuhandler.FeedMenuHandler
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.FeedEpisodeFilterDialog
import ac.mdiq.podcini.ui.dialog.FeedFilterDialog
import ac.mdiq.podcini.ui.dialog.FeedSortDialog
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
import ac.mdiq.podcini.ui.utils.CoverLoader
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
@ -48,13 +50,13 @@ import com.google.android.material.snackbar.Snackbar
import com.leinardi.android.speeddial.SpeedDialActionItem
import com.leinardi.android.speeddial.SpeedDialView
import io.realm.kotlin.query.RealmResults
import io.realm.kotlin.query.Sort
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
/**
@ -238,24 +240,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
adapter.setItems(feedListFiltered)
}
// fun filterOnTag() {
// when (tagFilterIndex) {
// 1 -> feedListFiltered = feedList // All feeds
// 0 -> feedListFiltered = feedList.filter { // feeds without tag
// val tags = it.preferences?.tags
// tags.isNullOrEmpty() || (tags.size == 1 && tags.toList()[0] == "#root")
// }
// else -> { // feeds with the chosen tag
// val tag = tags[tagFilterIndex]
// feedListFiltered = feedList.filter {
// it.preferences?.tags?.contains(tag) ?: false
// }
// }
// }
// binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
// adapter.setItems(feedListFiltered)
// }
private fun resetTags() {
tags.clear()
tags.add("Untagged")
@ -368,17 +352,23 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
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 feedList_ = getFeedList(fQueryStr).toMutableList()
val feeds_ = feedList_
val feedOrder = feedOrderBy
val dir = 1 - 2*feedOrderDir // get from 0, 1 to 1, -1
val comparator: Comparator<Feed> = when (feedOrder) {
FeedSortOrder.UNPLAYED_NEW_OLD.index -> {
val 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 queryString = "feedId == $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feeds_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find()
counterMap[f.id] = c
f.sortInfo = c.toString() + " unplayed"
}
comparator(counterMap, dir)
}
FeedSortOrder.ALPHABETIC_A_Z.index -> {
for (f in feeds_) f.sortInfo = ""
Comparator { lhs: Feed, rhs: Feed ->
val t1 = lhs.title
val t2 = rhs.title
@ -390,78 +380,83 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
}
}
FeedSortOrder.MOST_PLAYED.index -> {
val queryString = "feedId IN $0 AND playState == ${Episode.PLAYED}"
val episodes = realm.query(Episode::class).query(queryString, feedIds).find()
val counterMap = counterMap(episodes)
val queryString = "feedId == $0 AND playState == ${Episode.PLAYED}"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feeds_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find()
counterMap[f.id] = c
f.sortInfo = c.toString() + " played"
}
comparator(counterMap, dir)
}
FeedSortOrder.LAST_UPDATED_NEW_OLD.index -> {
val queryString = "feedId IN $0"
val episodes = realm.query(Episode::class, queryString, feedIds).sort("pubDate", Sort.DESCENDING).find()
val queryString = "feedId == $0 SORT(pubDate DESC)"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (episode in episodes) {
val feedId = episode.feedId ?: continue
val pDateOld = counterMap[feedId] ?: 0
if (pDateOld < episode.pubDate) counterMap[feedId] = episode.pubDate
for (f in feeds_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L
counterMap[f.id] = d
val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault())
f.sortInfo = "Updated on " + dateFormat.format(Date(d))
}
comparator(counterMap, dir)
}
FeedSortOrder.LAST_DOWNLOAD_NEW_OLD.index -> {
val queryString = "feedId IN $0"
val episodes = realm.query(Episode::class, queryString, feedIds).sort("media.downloadTime", Sort.DESCENDING).find()
val queryString = "feedId == $0 SORT(media.downloadTime DESC)"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (episode in episodes) {
val feedId = episode.feedId ?: continue
val pDownloadOld = counterMap[feedId] ?: 0
if (pDownloadOld < (episode.media?.downloadTime?:0)) counterMap[feedId] = episode.media?.downloadTime ?: 0
for (f in feeds_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.media?.downloadTime ?: 0L
counterMap[f.id] = d
val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault())
f.sortInfo = "Downloaded on " + dateFormat.format(Date(d))
}
comparator(counterMap, dir)
}
FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> {
val queryString = "feedId IN $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})"
val episodes = realm.query(Episode::class).query(queryString, feedIds).find()
val queryString = "feedId == $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) SORT(pubDate DESC)"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (episode in episodes) {
val feedId = episode.feedId ?: continue
val pDateOld = counterMap[feedId] ?: 0
if (pDateOld < episode.pubDate) counterMap[feedId] = episode.pubDate
for (f in feeds_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L
counterMap[f.id] = d
val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault())
f.sortInfo = "Unplayed since " + dateFormat.format(Date(d))
}
comparator(counterMap, dir)
}
FeedSortOrder.MOST_DOWNLOADED.index -> {
val queryString = "feedId IN $0 AND media.downloaded == true"
val episodes = realm.query(Episode::class).query(queryString, feedIds).find()
val counterMap = counterMap(episodes)
val queryString = "feedId == $0 AND media.downloaded == true"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feeds_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find()
counterMap[f.id] = c
f.sortInfo = c.toString() + " downloaded"
}
comparator(counterMap, dir)
}
FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> {
val queryString = "feedId IN $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true"
val episodes = realm.query(Episode::class).query(queryString, feedIds).find()
val counterMap = counterMap(episodes)
val queryString = "feedId == $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feeds_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find()
counterMap[f.id] = c
f.sortInfo = c.toString() + " downloaded unplayed"
}
comparator(counterMap, dir)
}
// doing FEED_ORDER_NEW
else -> {
val queryString = "feedId IN $0 AND playState == ${Episode.NEW}"
val episodes = realm.query(Episode::class).query(queryString, feedIds).find()
val counterMap = counterMap(episodes)
val queryString = "feedId == $0 AND playState == ${Episode.NEW}"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feeds_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find()
counterMap[f.id] = c
f.sortInfo = c.toString() + " new"
}
comparator(counterMap, dir)
}
}
val feedList_ = getFeedList(fQueryStr).toMutableList()
synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() }
}
private fun counterMap(episodes: RealmResults<Episode>): Map<Long, Long> {
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (episode in episodes) {
val feedId = episode.feedId ?: continue
val count = counterMap[feedId] ?: 0
counterMap[feedId] = count + 1
}
return counterMap
}
private fun comparator(counterMap: Map<Long, Long>, dir: Int): Comparator<Feed> {
return Comparator { lhs: Feed, rhs: Feed ->
val counterLhs = counterMap[lhs.id]?:0
@ -766,7 +761,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
private inner class ViewHolderExpanded(itemView: View) : RecyclerView.ViewHolder(itemView) {
val binding = SubscriptionItemBinding.bind(itemView)
val count: TextView = binding.countLabel
val count: TextView = binding.episodeCount
val coverImage: ImageView = binding.coverImage
val infoCard: LinearLayout = binding.infoCard
val selectView: FrameLayout = binding.selectContainer
@ -778,6 +773,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
selectView.background = drawable // Setting this in XML crashes API <= 21
binding.titleLabel.text = feed.title
binding.producerLabel.text = feed.author
binding.sortInfo.text = feed.sortInfo
coverImage.contentDescription = feed.title
coverImage.setImageDrawable(null)
@ -809,7 +805,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
private inner class ViewHolderBrief(itemView: View) : RecyclerView.ViewHolder(itemView) {
val binding = SubscriptionItemBriefBinding.bind(itemView)
private val title = binding.titleLabel
val count: TextView = binding.countLabel
val count: TextView = binding.episodeCount
val coverImage: ImageView = binding.coverImage
val selectView: FrameLayout = binding.selectContainer

View File

@ -35,6 +35,21 @@
android:layout_height="wrap_content"
android:text="@string/episodes_label"/>
<View
android:layout_width="15dp"
android:layout_height="match_parent"/>
<ImageButton
android:id="@+id/butShowSettings"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/show_feed_settings_label"
android:scaleType="fitXY"
android:padding="3dp"
app:srcCompat="@drawable/ic_settings_white"
tools:visibility="visible" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/select_queue_dialog"
android:padding="16dp">
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp">
</RadioGroup>
</LinearLayout>

View File

@ -57,13 +57,29 @@
android:lines="1"
android:text="Author" />
<TextView
android:id="@+id/countLabel"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/episodeCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:lines="1"
android:text="0 episodes" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<TextView
android:id="@+id/sortInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:text="info" />
</LinearLayout>
</LinearLayout>
<ImageView

View File

@ -64,7 +64,7 @@
tools:text="@sample/episodes.json/data/title" />
<TextView
android:id="@+id/countLabel"
android:id="@+id/episodeCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3"

View File

@ -1,20 +0,0 @@
<?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/switch_queue_dialog"
android:padding="16dp">
<Spinner
android:id="@+id/queue_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:dropDownWidth="200dp"
android:padding="8dp"
android:paddingBottom="20dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_micro"
android:spinnerMode="dropdown"/>
</LinearLayout>

View File

@ -38,9 +38,9 @@
android:title="@string/add_to_queue_label" />
<item
android:id="@+id/put_to_queue_batch"
android:id="@+id/put_in_queue_batch"
android:icon="@drawable/ic_playlist_play"
android:title="@string/put_to_queue_label" />
android:title="@string/put_in_queue_label" />
<item
android:id="@+id/add_to_favorite_batch"

View File

@ -146,7 +146,7 @@
<string name="feed_auto_download_newer">Newest unplayed</string>
<string name="feed_auto_download_older">Oldest unplayed</string>
<string name="put_to_queue_label">Put to queue</string>
<string name="put_in_queue_label">Put in queue</string>
<string name="feed_new_episodes_action_nothing">Nothing</string>
<string name="episode_cleanup_never">Never</string>
@ -488,6 +488,8 @@
<string name="pref_automatic_download_on_battery_sum">Allow automatic download when the battery is not charging</string>
<string name="pref_episode_cache_title">Episode cache</string>
<string name="pref_episode_cache_summary">Total number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached.</string>
<string name="pref_auto_download_counting_played_title">Counting played</string>
<string name="pref_auto_download_counting_played_summary">Set if downloaded episodes already played count into the episode cache</string>
<string name="pref_episode_cover_title">Use episode cover</string>
<string name="pref_episode_cover_summary">Use the episode specific cover in lists whenever available. If unchecked, the app will always use the podcast cover image.</string>
<string name="pref_show_remain_time_title">Show remaining time</string>

View File

@ -67,6 +67,10 @@
android:title="@string/pref_episode_cache_title"
android:summary="@string/pref_episode_cache_summary"
android:entryValues="@array/feed_episode_cache_size_values"/>
<SwitchPreferenceCompat
android:key="countingPlayed"
android:summary="@string/pref_auto_download_counting_played_summary"
android:title="@string/pref_auto_download_counting_played_title" />
<Preference
android:key="episodeInclusiveFilter"
android:summary="@string/episode_filters_description"

View File

@ -1,3 +1,16 @@
# 6.1.3
* added feed setting in the header of FeedInfo view
* in all episodes list views, click on an episode image brings up the FeedInfo view
* added countingPlayed for auto download in feed setting, when set to false, downloaded episodes that have been played are not counted as downloaded to the limit of auto-download
* fixed possible mal-function of feed sorting
* improved feed sorting efficiency
* improved feed update efficiency
* in Subscriptions view added sorting info on every feed (List Layout only)
* "Put to queue" text changed to "Put in queue"
* in dialogs "Put in queue" and "Switch queue" the spinner is changed to lisst of radio buttons
* likely fixed hang when switching queue sometimes
# 6.1.2
* fixed crash issue when setting the inclusive or exclusive filters in feed auto-download setting

View File

@ -0,0 +1,13 @@
Version 6.1.3 brings several changes:
* added feed setting in the header of FeedInfo view
* in all episodes list views, click on an episode image brings up the FeedInfo view
* added countingPlayed for auto download in feed setting, when set to false, downloaded episodes that have been played are not counted as downloaded to the limit of auto-download
* fixed possible mal-function of feed sorting
* improved feed sorting efficiency
* improved feed update efficiency
* in Subscriptions view added sorting info on every feed (List Layout only)
* "Put to queue" text changed to "Put in queue"
* in dialogs "Put in queue" and "Switch queue" the spinner is changed to lisst of radio buttons
* likely fixed hang when switching queue sometimes