diff --git a/README.md b/README.md index c62c5db7..5a4bc9e7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index b4f6c8be..4bb8523b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt index 01cdbf1d..21ad5c0d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt @@ -332,10 +332,9 @@ 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 } + downloadStatus = DownloadResult(feed.id, feed.getTextIdentifier()?:"", reason?: DownloadError.ERROR_NOT_FOUND, isSuccessful, reasonDetailed?:"") + return null } /** * Checks if the feed was parsed correctly. diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt index 24cf7254..379898cd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt @@ -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? = 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 = 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 = 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 = 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 = 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() - 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()) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index 22b6cae3..30aa752d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -36,16 +36,12 @@ import kotlin.math.abs object Feeds { private val TAG: String = Feeds::class.simpleName ?: "Anonymous" - private val feedMap: MutableMap = mutableMapOf() private val tags: MutableList = mutableListOf() @Synchronized - fun getFeedList(queryString: String = "", fromDB: Boolean = true): List { - if (fromDB) { - return if (queryString.isEmpty()) realm.query(Feed::class).find() - else realm.query(Feed::class, queryString).find() - } - return feedMap.values.toList() + fun getFeedList(queryString: String = ""): List { + return if (queryString.isEmpty()) realm.query(Feed::class).find() + else realm.query(Feed::class, queryString).find() } fun getFeedCount(): Int { @@ -56,26 +52,6 @@ object Feeds { return tags } - @Synchronized - fun updateFeedMap(feeds: List = 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() 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) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index 1a7fc2ab..5dc0a08c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index 91843efa..b4faf82b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt index b6ebcf27..7635fbd6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt index 2cba04a8..70e7b59d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt @@ -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. */ diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt index e057ce4b..25560eae 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt @@ -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), diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt index 8fb59e5c..ce917551 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt @@ -19,39 +19,23 @@ object EpisodesPermutors { var permutor: Permutor? = 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 { override fun reorder(queue: MutableList?) { if (!queue.isNullOrEmpty()) queue.shuffle() @@ -67,10 +51,8 @@ object EpisodesPermutors { if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, 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 = comparator diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt index d8512a5a..9591609a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMultiSelectHandler.kt @@ -1,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) { - PutToQueueDialog(activity as MainActivity, items).show() - } - class PutToQueueDialog(activity: Activity, val items: List) { private val activityRef: WeakReference = WeakReference(activity) fun show() { val activity = activityRef.get() ?: return - val binding = SwitchQueueDialogBinding.inflate(LayoutInflater.from(activity)) + val 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)) + for (i in queues.indices) { + val radioButton = RadioButton(activity) + radioButton.text = queues[i].name + radioButton.textSize = 20f + radioButton.tag = i + binding.radioGroup.addView(radioButton) + } 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<*>?) {} + binding.radioGroup.setOnCheckedChangeListener { group, checkedId -> + val radioButton = group.findViewById(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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt index 016a7161..922e715f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt @@ -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 } /** diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt index 5eede183..600ee63d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt @@ -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,32 +23,36 @@ 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] - } - override fun onNothingSelected(parent: AdapterView<*>?) {} + 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) + } + binding.radioGroup.setOnCheckedChangeListener { group, checkedId -> + binding.radioGroup.check(checkedId) + val radioButton = group.findViewById(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 -> - val items = mutableListOf() - items.addAll(curQueue.episodes) - items.addAll(curQueue_.episodes) - curQueue = realm.copyFromRealm(curQueue_) - curQueue.update() - upsertBlk(curQueue) {} - EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items)) + if (curQueue_.id != curQueue.id) { + val items = mutableListOf() + items.addAll(curQueue.episodes) + items.addAll(curQueue_.episodes) + curQueue = unmanaged(curQueue_) + curQueue.update() + upsertBlk(curQueue) {} + EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items)) + } } .setNegativeButton(R.string.cancel_label, null) .show() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index b4b74578..9f7e2125 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -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) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index 4bceb9c0..1bca4d0b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index 1e3ab235..77bed85d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -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") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index 9e865c5f..a5db65a0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -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(Prefs.feedEpisodeCacheSize.name) - cachePref!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> + val cachePref = findPreference(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(Prefs.feedEpisodeCacheSize.name) - cachePref!!.value = feedPrefs!!.autoDLMaxEpisodes.toString() + val pref = findPreference(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(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("keepUpdated") + val pref = findPreference(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(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = false + findPreference(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = false + findPreference(Prefs.countingPlayed.name)!!.isEnabled = false findPreference(Prefs.episodeInclusiveFilter.name)!!.isEnabled = false findPreference(Prefs.episodeExclusiveFilter.name)!!.isEnabled = false - findPreference(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(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = enabled + findPreference(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = enabled + findPreference(Prefs.countingPlayed.name)!!.isEnabled = enabled findPreference(Prefs.episodeInclusiveFilter.name)!!.isEnabled = enabled findPreference(Prefs.episodeExclusiveFilter.name)!!.isEnabled = enabled - findPreference(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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index 85bff499..7048a367 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index c6feee81..96af0e01 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -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++ } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index b7e385b8..c4fd59ff 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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): Map { - val counterMap: MutableMap = 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, dir: Int): Comparator { 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 diff --git a/app/src/main/res/layout/feedinfo_header.xml b/app/src/main/res/layout/feedinfo_header.xml index 79e9349e..a42b2453 100644 --- a/app/src/main/res/layout/feedinfo_header.xml +++ b/app/src/main/res/layout/feedinfo_header.xml @@ -35,6 +35,21 @@ android:layout_height="wrap_content" android:text="@string/episodes_label"/> + + + + + + + + + + + diff --git a/app/src/main/res/layout/subscription_item.xml b/app/src/main/res/layout/subscription_item.xml index 3919dee8..7c556534 100644 --- a/app/src/main/res/layout/subscription_item.xml +++ b/app/src/main/res/layout/subscription_item.xml @@ -57,13 +57,29 @@ android:lines="1" android:text="Author" /> - + android:orientation="horizontal"> + + + + + + - - - - - diff --git a/app/src/main/res/menu/episodes_apply_action_speeddial.xml b/app/src/main/res/menu/episodes_apply_action_speeddial.xml index f91d9f91..e110ee6f 100644 --- a/app/src/main/res/menu/episodes_apply_action_speeddial.xml +++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml @@ -38,9 +38,9 @@ android:title="@string/add_to_queue_label" /> + android:title="@string/put_in_queue_label" /> Newest unplayed Oldest unplayed - Put to queue + Put in queue Nothing Never @@ -488,6 +488,8 @@ Allow automatic download when the battery is not charging Episode cache Total number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached. + Counting played + Set if downloaded episodes already played count into the episode cache Use episode cover Use the episode specific cover in lists whenever available. If unchecked, the app will always use the podcast cover image. Show remaining time diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml index 3d109a08..7c603604 100644 --- a/app/src/main/res/xml/feed_settings.xml +++ b/app/src/main/res/xml/feed_settings.xml @@ -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"/> +