mirror of
https://github.com/XilinJia/Podcini.git
synced 2025-01-31 06:07:05 +01:00
6.0.1 commit
This commit is contained in:
parent
2a66e7fb5d
commit
393063ad09
25
README.md
25
README.md
@ -21,11 +21,12 @@ Compared to AntennaPod this project:
|
||||
1. Migrated all media routines to `androidx.media3`, with `AudioOffloadMode` enabled, nicer to device battery,
|
||||
2. Is purely `Kotlin` based and mono-modular, and targets Android 14,
|
||||
3. Iron-age celebrity SQLite is replaced with modern object-base Realm DB
|
||||
4. Outfits with Viewbinding, Coil replacing Glide, coroutines replacing RxJava and threads, and SharedFlow replacing EventBus,
|
||||
5. Boasts new UI's including streamlined drawer, subscriptions view and player controller,
|
||||
6. Accepts podcast as well as plain RSS and YouTube feeds,
|
||||
7. Offers Readability and Text-to-Speech for RSS contents,
|
||||
8. Features `instant sync` across devices without a server.
|
||||
4. Removed the need for support libraries and jetifier
|
||||
5. Outfits with Viewbinding, Coil replacing Glide, coroutines replacing RxJava and threads, and SharedFlow replacing EventBus,
|
||||
6. Boasts new UI's including streamlined drawer, subscriptions view and player controller,
|
||||
7. Accepts podcast as well as plain RSS and YouTube feeds,
|
||||
8. Offers Readability and Text-to-Speech for RSS contents,
|
||||
9. Features `instant sync` across devices without a server.
|
||||
|
||||
The project aims to profit from modern frameworks, improve efficiency and provide more useful and user-friendly features.
|
||||
|
||||
@ -75,16 +76,27 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
|
||||
* Played episodes have clearer markings
|
||||
* Sort dialog no longer dims the main 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)
|
||||
* Subscriptions view has sorting by "Unread publication date"
|
||||
* Long-press filter button in FeedEpisode view enables/disables filters without changing filter settings
|
||||
* Subscriptions view has various explicit measures for sorting
|
||||
* 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
|
||||
* 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
|
||||
* on app startup, the most recently updated queue is set to curQueue
|
||||
* queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
|
||||
* on action bar of FeedEpisodes view there is a direct access to Queue
|
||||
|
||||
|
||||
### 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 has button showing number of episodes to open the FeedEpisodes view
|
||||
* 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
|
||||
* RSS feeds with no playable media can be subscribed and read/listened (via TTS)
|
||||
* deleting feeds is performed promptly
|
||||
|
||||
### Online feed
|
||||
|
||||
@ -107,6 +119,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
|
||||
* Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure
|
||||
* Settings/Preferences can now be exported and imported
|
||||
* Play history/progress can be separately exported/imported as Json files
|
||||
* There is a setting to disable/enable auto backup OPML files to Google
|
||||
|
||||
For more details of the changes, see the [Changelog](changelog.md)
|
||||
|
||||
|
@ -108,21 +108,27 @@ android {
|
||||
// start of the app build.gradle
|
||||
android {
|
||||
namespace "ac.mdiq.podcini"
|
||||
lintOptions {
|
||||
disable 'ObsoleteLintCustomCheck', 'CheckResult', 'UnusedAttribute', 'BatteryLife', 'InflateParams',
|
||||
lint {
|
||||
lintConfig = file("lint.xml")
|
||||
// checkOnly += ['NewApi', 'InlinedApi']
|
||||
checkOnly += ['NewApi', 'InlinedApi', 'UnusedResources', 'ObsoleteSdkInt',
|
||||
'Performance', 'ViewId', 'MissingTranslation',
|
||||
'Deprecation', 'DuplicateIds', 'UseSparseArrays']
|
||||
|
||||
disable += ['TypographyDashes', 'TypographyQuotes', 'ObsoleteLintCustomCheck', 'CheckResult', 'UnusedAttribute', 'BatteryLife', 'InflateParams',
|
||||
'RestrictedApi', 'TrustAllX509TrustManager', 'ExportedReceiver', 'AllowBackup', 'VectorDrawableCompat',
|
||||
'StaticFieldLeak', 'UseCompoundDrawables', 'NestedWeights', 'Overdraw', 'UselessParent', 'TextFields',
|
||||
'AlwaysShowAction', 'Autofill', 'ClickableViewAccessibility', 'ContentDescription',
|
||||
'KeyboardInaccessibleWidget', 'LabelFor', 'SetTextI18n', 'HardcodedText', 'RelativeOverlap',
|
||||
'RtlCompat', 'RtlHardcoded', 'MissingMediaBrowserServiceIntentFilter', 'VectorPath',
|
||||
'InvalidPeriodicWorkRequestInterval', 'NotifyDataSetChanged', 'RtlEnabled'
|
||||
'InvalidPeriodicWorkRequestInterval', 'NotifyDataSetChanged', 'RtlEnabled']
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
defaultConfig {
|
||||
versionCode 3020200
|
||||
versionName "6.0.0"
|
||||
versionCode 3020201
|
||||
versionName "6.0.1"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
@ -22,6 +22,7 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.persistEpisodes
|
||||
import ac.mdiq.podcini.storage.database.Feeds.deleteFeed
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedListDownloadUrls
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||
@ -161,7 +162,7 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont
|
||||
private fun removeFeedWithDownloadUrl(context: Context, downloadUrl: String) {
|
||||
Logd(TAG, "removeFeedWithDownloadUrl called")
|
||||
var feedID: Long? = null
|
||||
val feeds = realm.query(Feed::class).find()
|
||||
val feeds = getFeedList()
|
||||
for (f in feeds) {
|
||||
val url = f.downloadUrl
|
||||
if (url != null && !url.startsWith(Feed.PREFIX_LOCAL_FOLDER)) feedID = f.id
|
||||
|
@ -12,8 +12,8 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.utils.SortOrder
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.storage.utils.SortOrder
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
|
@ -27,10 +27,10 @@ object InTheatre {
|
||||
}
|
||||
|
||||
var curMedia: Playable? = null
|
||||
get() {
|
||||
if (field == null) field = loadPlayableFromPreferences()
|
||||
return field
|
||||
}
|
||||
// get() {
|
||||
// if (field == null) field = loadPlayableFromPreferences()
|
||||
// return field
|
||||
// }
|
||||
set(value) {
|
||||
field = value
|
||||
if (field is EpisodeMedia) {
|
||||
@ -83,6 +83,7 @@ object InTheatre {
|
||||
copyToRealm(curState_)
|
||||
}
|
||||
}
|
||||
loadPlayableFromPreferences()
|
||||
}
|
||||
// val curState_ = realm.query(CurrentState::class).first()
|
||||
// val job = CoroutineScope(Dispatchers.Default).launch {
|
||||
@ -114,7 +115,7 @@ object InTheatre {
|
||||
* depending on the type of playable that was restored.
|
||||
* @return The restored Playable object
|
||||
*/
|
||||
fun loadPlayableFromPreferences(): Playable? {
|
||||
fun loadPlayableFromPreferences() {
|
||||
Logd(TAG, "loadPlayableFromPreferences currentlyPlayingType: $curState.curMediaType")
|
||||
if (curState.curMediaType != NO_MEDIA_PLAYING) {
|
||||
val type = curState.curMediaType.toInt()
|
||||
@ -124,13 +125,8 @@ object InTheatre {
|
||||
curMedia = getEpisodeMedia(mediaId)
|
||||
if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episode
|
||||
}
|
||||
return curMedia
|
||||
} else {
|
||||
Log.e(TAG, "Could not restore Playable object from preferences")
|
||||
return null
|
||||
}
|
||||
} else Log.e(TAG, "Could not restore Playable object from preferences")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) @JvmStatic
|
||||
|
@ -230,7 +230,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
|
||||
*/
|
||||
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
|
||||
Logd(TAG, "playMediaObject $forceReset $stream $startWhenPrepared $prepareImmediately $status ${curMedia?.getEpisodeTitle()} ")
|
||||
Logd(TAG, "playMediaObject $forceReset $stream $startWhenPrepared $prepareImmediately $status ${playable.getEpisodeTitle()} ")
|
||||
if (curMedia != null) {
|
||||
if (!forceReset && curMedia!!.getIdentifier() == prevMedia?.getIdentifier() && status == PlayerStatus.PLAYING) {
|
||||
// episode is already playing -> ignore method call
|
||||
@ -255,10 +255,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, null)
|
||||
}
|
||||
}
|
||||
// else {
|
||||
// Log.e(TAG, "playMediaObject curMedia is null")
|
||||
// return
|
||||
// }
|
||||
|
||||
curMedia = playable
|
||||
this.isStreaming = stream
|
||||
|
@ -54,6 +54,7 @@ import ac.mdiq.podcini.storage.model.FeedPreferences
|
||||
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.indexOfItemWithId
|
||||
import ac.mdiq.podcini.storage.utils.MediaType
|
||||
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
@ -68,6 +69,7 @@ import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.bluetooth.BluetoothA2dp
|
||||
import android.content.*
|
||||
import android.content.Intent.EXTRA_KEY_EVENT
|
||||
@ -259,7 +261,10 @@ class PlaybackService : MediaSessionService() {
|
||||
|
||||
recreateMediaPlayer()
|
||||
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||
mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!)
|
||||
.setSessionActivity(pendingIntent)
|
||||
.setCallback(MyCallback())
|
||||
.setCustomLayout(notificationCustomButtons)
|
||||
.build()
|
||||
@ -845,7 +850,7 @@ class PlaybackService : MediaSessionService() {
|
||||
return nextItem.media
|
||||
}
|
||||
|
||||
fun getNextInQueue(episode: Episode): Episode? {
|
||||
private fun getNextInQueue(episode: Episode): Episode? {
|
||||
Logd(TAG, "getNextInQueue() with: itemId ${episode.id}")
|
||||
if (curQueue.episodes.isEmpty()) return null
|
||||
|
||||
|
@ -4,7 +4,6 @@ import ac.mdiq.podcini.net.download.DownloadError
|
||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.feedOrder
|
||||
import ac.mdiq.podcini.storage.database.Episodes.EpisodeDuplicateGuesser
|
||||
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
|
||||
import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus
|
||||
@ -28,7 +27,6 @@ import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import io.realm.kotlin.query.RealmResults
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
@ -37,29 +35,29 @@ import java.util.concurrent.ExecutionException
|
||||
|
||||
object Feeds {
|
||||
private val TAG: String = Feeds::class.simpleName ?: "Anonymous"
|
||||
internal val feeds: MutableList<Feed> = mutableListOf()
|
||||
// internal val feeds: MutableList<Feed> = mutableListOf()
|
||||
private val feedMap: MutableMap<Long, Feed> = mutableMapOf()
|
||||
private val tags: MutableList<String> = mutableListOf()
|
||||
|
||||
fun getFeedList(): List<Feed> {
|
||||
return feeds
|
||||
// return realm.query(Feed::class).find()
|
||||
return feedMap.values.toList()
|
||||
}
|
||||
|
||||
fun getTags(): List<String> {
|
||||
return tags
|
||||
}
|
||||
|
||||
fun updateFeedList() {
|
||||
Logd(TAG, "updateFeedList called")
|
||||
fun updateFeedMap() {
|
||||
Logd(TAG, "updateFeedMap called")
|
||||
val feeds_ = realm.query(Feed::class).find()
|
||||
feeds.clear()
|
||||
feeds.addAll(feeds_)
|
||||
feedMap.clear()
|
||||
feedMap.putAll(feeds_.associateBy { it.id })
|
||||
buildTags()
|
||||
}
|
||||
|
||||
fun buildTags() {
|
||||
val tagsSet = mutableSetOf<String>()
|
||||
val feedsCopy = feeds.toList()
|
||||
val feedsCopy = feedMap.values
|
||||
for (feed in feedsCopy) {
|
||||
if (feed.preferences != null) {
|
||||
for (tag in feed.preferences!!.tags) {
|
||||
@ -75,58 +73,36 @@ object Feeds {
|
||||
fun getFeedListDownloadUrls(): List<String> {
|
||||
Logd(TAG, "getFeedListDownloadUrls called")
|
||||
val result: MutableList<String> = mutableListOf()
|
||||
val feeds = realm.query(Feed::class).find()
|
||||
for (f in feeds) {
|
||||
// val feeds = realm.query(Feed::class).find()
|
||||
for (f in feedMap.values) {
|
||||
val url = f.downloadUrl
|
||||
if (url != null && !url.startsWith(Feed.PREFIX_LOCAL_FOLDER)) result.add(url)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun getFeed(feedId: Long): Feed? {
|
||||
Logd(TAG, "getFeed() called with: $feedId")
|
||||
val f = realm.query(Feed::class).query("id == $0", feedId).first().find()
|
||||
return if (f != null) realm.copyFromRealm(f) else null
|
||||
// TODO: some callers don't need to copy
|
||||
fun getFeed(feedId: Long, copy: Boolean = true): Feed? {
|
||||
// Logd(TAG, "getFeed() called with: $feedId")
|
||||
// val f = realm.query(Feed::class).query("id == $0", feedId).first().find()
|
||||
// return if (f != null && f.isManaged()) realm.copyFromRealm(f) else null
|
||||
val f = feedMap[feedId]
|
||||
return if (f != null) {
|
||||
if (copy) realm.copyFromRealm(f)
|
||||
else f
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun searchFeedByIdentifyingValueOrID(feed: Feed): Feed? {
|
||||
Logd(TAG, "searchFeedByIdentifyingValueOrID called")
|
||||
if (feed.id != 0L) return getFeed(feed.id)
|
||||
val feeds = getFeedList()
|
||||
for (f in feeds.toList()) {
|
||||
if (f.identifyingValue == feed.identifyingValue) {
|
||||
// f.episodes.clear()
|
||||
// f.episodes.addAll(getFeedItemList(f))
|
||||
return f
|
||||
}
|
||||
for (f in feeds) {
|
||||
if (f.identifyingValue == feed.identifyingValue) return f
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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>): Comparator<Feed> {
|
||||
return Comparator { lhs: Feed, rhs: Feed ->
|
||||
val counterLhs = counterMap[lhs.id]?:0
|
||||
val counterRhs = counterMap[rhs.id]?:0
|
||||
when {
|
||||
// reverse natural order: podcast with most unplayed episodes first
|
||||
counterLhs > counterRhs -> -1
|
||||
counterLhs == counterRhs -> lhs.title?.compareTo(rhs.title!!, ignoreCase = true) ?: -1
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------ writer ----------------------
|
||||
|
||||
/**
|
||||
@ -186,8 +162,7 @@ object Feeds {
|
||||
val possibleDuplicate = searchEpisodeGuessDuplicate(newFeed.episodes, episode)
|
||||
if (!newFeed.isLocalFeed && possibleDuplicate != null && episode !== possibleDuplicate) {
|
||||
// Canonical episode is the first one returned (usually oldest)
|
||||
addDownloadStatus(DownloadResult(savedFeed.id,
|
||||
episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false,
|
||||
addDownloadStatus(DownloadResult(savedFeed.id, episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false,
|
||||
"""
|
||||
The podcast host appears to have added the same episode twice. Podcini still refreshed the feed and attempted to repair it.
|
||||
|
||||
@ -235,7 +210,7 @@ object Feeds {
|
||||
else {
|
||||
Logd(TAG, "Found new episode: ${episode.title}")
|
||||
episode.feed = savedFeed
|
||||
episode.id = idLong
|
||||
episode.id = idLong++
|
||||
episode.feedId = savedFeed.id
|
||||
if (episode.media != null) episode.media!!.id = episode.id
|
||||
|
||||
@ -247,7 +222,7 @@ object Feeds {
|
||||
Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate")
|
||||
episode.setNew()
|
||||
}
|
||||
idLong += 1
|
||||
// idLong += 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,7 +253,7 @@ object Feeds {
|
||||
} else {
|
||||
persistFeedsSync(savedFeed)
|
||||
}
|
||||
updateFeedList()
|
||||
updateFeedMap()
|
||||
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() }
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
@ -309,7 +284,6 @@ object Feeds {
|
||||
*/
|
||||
private fun searchEpisodeGuessDuplicate(episodes: List<Episode>?, searchItem: Episode): Episode? {
|
||||
if (episodes.isNullOrEmpty()) return null
|
||||
|
||||
for (episode in episodes) {
|
||||
if (EpisodeDuplicateGuesser.sameAndNotEmpty(episode.identifier, searchItem.identifier)) return episode
|
||||
}
|
||||
@ -330,75 +304,6 @@ object Feeds {
|
||||
""".trimIndent()))
|
||||
}
|
||||
|
||||
fun sortFeeds() {
|
||||
Logd(TAG, "sortFeeds() called")
|
||||
val feedOrder = feedOrder
|
||||
val comparator: Comparator<Feed> = when (feedOrder) {
|
||||
UserPreferences.FEED_ORDER_UNPLAYED -> {
|
||||
val episodes = realm.query(Episode::class).query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_ALPHABETICAL -> {
|
||||
Comparator { lhs: Feed, rhs: Feed ->
|
||||
val t1 = lhs.title
|
||||
val t2 = rhs.title
|
||||
when {
|
||||
t1 == null -> 1
|
||||
t2 == null -> -1
|
||||
else -> t1.compareTo(t2, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPreferences.FEED_ORDER_MOST_PLAYED -> {
|
||||
val episodes = realm.query(Episode::class).query("playState == ${Episode.PLAYED}").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_LAST_UPDATED -> {
|
||||
val episodes = realm.query(Episode::class).find()
|
||||
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
|
||||
}
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_LAST_UNREAD_UPDATED -> {
|
||||
val episodes = realm.query(Episode::class)
|
||||
.query("playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}").find()
|
||||
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
|
||||
}
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_DOWNLOADED -> {
|
||||
val episodes = realm.query(Episode::class).query("media.downloaded == 1").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_DOWNLOADED_UNPLAYED -> {
|
||||
val episodes = realm.query(Episode::class)
|
||||
.query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == 1").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
// doing FEED_ORDER_NEW
|
||||
else -> {
|
||||
val episodes = realm.query(Episode::class).query("playState == ${Episode.NEW}").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
}
|
||||
synchronized(feeds) {
|
||||
feeds.sortWith(comparator)
|
||||
}
|
||||
}
|
||||
|
||||
fun persistFeedLastUpdateFailed(feed: Feed, lastUpdateFailed: Boolean) : Job {
|
||||
Logd(TAG, "persistFeedLastUpdateFailed called")
|
||||
return runOnIOScope {
|
||||
@ -408,9 +313,6 @@ object Feeds {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates download URL of a feed
|
||||
*/
|
||||
fun updateFeedDownloadURL(original: String, updated: String) : Job {
|
||||
Logd(TAG, "updateFeedDownloadURL(original: $original, updated: $updated)")
|
||||
return runOnIOScope {
|
||||
@ -436,11 +338,11 @@ object Feeds {
|
||||
|
||||
Logd(TAG, "feed.episodes: ${feed.episodes.size}")
|
||||
for (episode in feed.episodes) {
|
||||
episode.id = idLong
|
||||
episode.id = idLong++
|
||||
episode.feedId = feed.id
|
||||
if (episode.media != null) episode.media!!.id = episode.id
|
||||
// copyToRealm(episode) // no need if episodes is a relation of feed, otherwise yes.
|
||||
idLong += 1
|
||||
// idLong += 1
|
||||
}
|
||||
copyToRealm(feed)
|
||||
}
|
||||
@ -478,28 +380,7 @@ object Feeds {
|
||||
* @param context A context that is used for opening a database connection.
|
||||
* @param feedId ID of the Feed that should be deleted.
|
||||
*/
|
||||
// fun deleteFeed0(context: Context, feedId: Long) : Job {
|
||||
// Logd(TAG, "deleteFeed called")
|
||||
// return runOnDbThread {
|
||||
// var feed = getFeed(feedId)
|
||||
// if (feed != null) {
|
||||
// deleteEpisodesSync(context, feed.episodes)
|
||||
// realm.write {
|
||||
// val feed_ = query(Feed::class).query("id == $0", feedId).first().find()
|
||||
// if (feed_ != null) delete(feed_)
|
||||
// }
|
||||
// if (!feed.isLocalFeed && feed.downloadUrl != null) SynchronizationQueueSink.enqueueFeedRemovedIfSyncActive(context, feed.downloadUrl!!)
|
||||
// EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Deletes a Feed and all downloaded files of its components like images and downloaded episodes.
|
||||
* @param context A context that is used for opening a database connection.
|
||||
* @param feedId ID of the Feed that should be deleted.
|
||||
*/
|
||||
fun deleteFeed(context: Context, feedId: Long) : Job {
|
||||
fun deleteFeed(context: Context, feedId: Long, postEvent: Boolean = true) : Job {
|
||||
Logd(TAG, "deleteFeed called")
|
||||
return runOnIOScope {
|
||||
val feed = getFeed(feedId)
|
||||
@ -519,7 +400,7 @@ object Feeds {
|
||||
}
|
||||
}
|
||||
if (!feed.isLocalFeed && feed.downloadUrl != null) SynchronizationQueueSink.enqueueFeedRemovedIfSyncActive(context, feed.downloadUrl!!)
|
||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed))
|
||||
if (postEvent) EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ object LogsAndStats {
|
||||
Logd(TAG, "addDownloadStatus called")
|
||||
return runOnIOScope {
|
||||
if (status != null) {
|
||||
if (status.id == 0L) status.id = Date().time
|
||||
if (status.id == 0L) status.setId()
|
||||
realm.write {
|
||||
copyToRealm(status)
|
||||
}
|
||||
@ -48,7 +48,7 @@ object LogsAndStats {
|
||||
val result = StatisticsResult()
|
||||
result.oldestDate = Long.MAX_VALUE
|
||||
for (fid in groupdMedias.keys) {
|
||||
val feed = getFeed(fid) ?: continue
|
||||
val feed = getFeed(fid, false) ?: continue
|
||||
val episodes = feed.episodes.size.toLong()
|
||||
var feedPlayedTime = 0L
|
||||
var feedTotalTime = 0L
|
||||
|
@ -34,6 +34,7 @@ class DownloadResult(
|
||||
var reasonDetailed: String) : RealmObject {
|
||||
|
||||
@PrimaryKey var id: Long = 0L
|
||||
private set
|
||||
|
||||
@Ignore
|
||||
private val completionDate = completionDate.clone() as Date
|
||||
@ -43,13 +44,18 @@ class DownloadResult(
|
||||
/**
|
||||
* Constructor for creating new completed downloads.
|
||||
*/
|
||||
constructor(id: Long, title: String, reason: DownloadError, successful: Boolean, reasonDetailed: String)
|
||||
: this(title, id, EpisodeMedia.FEEDFILETYPE_FEEDMEDIA, successful, reason, Date(), reasonDetailed)
|
||||
constructor(feedId: Long, title: String, reason: DownloadError, successful: Boolean, reasonDetailed: String)
|
||||
: this(title, feedId, EpisodeMedia.FEEDFILETYPE_FEEDMEDIA, successful, reason, Date(), reasonDetailed)
|
||||
|
||||
override fun toString(): String {
|
||||
return ("DownloadStatus [id=$id, title=$title, reason=$reason, reasonDetailed=$reasonDetailed, successful=$isSuccessful, completionDate=$completionDate, feedfileId=$feedfileId, feedfileType=$feedfileType]")
|
||||
}
|
||||
|
||||
fun setId() {
|
||||
if (idCounter < 0) idCounter = Date().time
|
||||
id = idCounter++
|
||||
}
|
||||
|
||||
fun getCompletionDate(): Date {
|
||||
return completionDate.clone() as Date
|
||||
}
|
||||
@ -76,5 +82,7 @@ class DownloadResult(
|
||||
* so that the listadapters etc. can react properly.
|
||||
*/
|
||||
const val SIZE_UNKNOWN: Int = -1
|
||||
|
||||
var idCounter: Long = -1
|
||||
}
|
||||
}
|
@ -198,7 +198,6 @@ class Episode : RealmObject {
|
||||
if (other.podcastIndexChapterUrl != null) podcastIndexChapterUrl = other.podcastIndexChapterUrl
|
||||
}
|
||||
|
||||
|
||||
@JvmName("getPubDateFunction")
|
||||
fun getPubDate(): Date? {
|
||||
return if (pubDate > 0) Date(pubDate) else null
|
||||
@ -220,7 +219,6 @@ class Episode : RealmObject {
|
||||
this.media = media
|
||||
}
|
||||
|
||||
|
||||
fun setNew() {
|
||||
playState = NEW
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.defaultPage
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
import ac.mdiq.podcini.receiver.PlayerWidget
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedMap
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
@ -129,7 +129,7 @@ class MainActivity : CastEnabledActivity() {
|
||||
NavDrawerFragment.getSharedPrefs(this@MainActivity)
|
||||
SwipeActions.getSharedPrefs(this@MainActivity)
|
||||
QueueFragment.getSharedPrefs(this@MainActivity)
|
||||
updateFeedList()
|
||||
updateFeedMap()
|
||||
// InTheatre.apply { }
|
||||
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
||||
PlayerWidget.getSharedPrefs(this@MainActivity)
|
||||
|
@ -27,7 +27,7 @@ class DownloadLogDetailsDialog(context: Context, status: DownloadResult) : Mater
|
||||
if (media != null) url = media.downloadUrl?:""
|
||||
}
|
||||
Feed.FEEDFILETYPE_FEED -> {
|
||||
val feed = getFeed(status.feedfileId)
|
||||
val feed = getFeed(status.feedfileId, false)
|
||||
if (feed != null) url = feed.downloadUrl?:""
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Feeds.deleteFeed
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
@ -31,7 +33,6 @@ object RemoveFeedDialog {
|
||||
val dialog: ConfirmationDialog = object : ConfirmationDialog(context, R.string.remove_feed_label, message) {
|
||||
@OptIn(UnstableApi::class) override fun onConfirmButtonPressed(clickedDialog: DialogInterface) {
|
||||
callback?.run()
|
||||
|
||||
clickedDialog.dismiss()
|
||||
|
||||
val progressDialog = ProgressDialog(context)
|
||||
@ -46,8 +47,9 @@ object RemoveFeedDialog {
|
||||
withContext(Dispatchers.IO) {
|
||||
for (feed in feeds) {
|
||||
// runBlocking { deleteFeed(context, feed.id).join() }
|
||||
deleteFeed(context, feed.id)
|
||||
deleteFeed(context, feed.id, false)
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feeds))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Logd(TAG, "Feed(s) deleted")
|
||||
|
@ -15,6 +15,7 @@ import ac.mdiq.podcini.playback.PlaybackController.Companion.position
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.loadPlayableFromPreferences
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
@ -96,8 +97,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||
private var playerFragment2: InternalPlayerFragment? = null
|
||||
private var playerFragment: InternalPlayerFragment? = null
|
||||
|
||||
private lateinit var playerView1: View
|
||||
private lateinit var playerView2: View
|
||||
private var playerView1: View? = null
|
||||
private var playerView2: View? = null
|
||||
|
||||
private lateinit var cardViewSeek: CardView
|
||||
private lateinit var txtvSeek: TextView
|
||||
@ -140,14 +141,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||
.replace(R.id.playerFragment1, playerFragment1!!, "InternalPlayerFragment1")
|
||||
.commit()
|
||||
playerView1 = binding.root.findViewById(R.id.playerFragment1)
|
||||
playerView1.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
playerView1?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
|
||||
playerFragment2 = InternalPlayerFragment.newInstance(controller!!)
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.playerFragment2, playerFragment2!!, "InternalPlayerFragment2")
|
||||
.commit()
|
||||
playerView2 = binding.root.findViewById(R.id.playerFragment2)
|
||||
playerView2.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
playerView2?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
|
||||
onCollaped()
|
||||
|
||||
@ -463,8 +464,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||
fun fadePlayerToToolbar(slideOffset: Float) {
|
||||
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat()
|
||||
val player = playerView1
|
||||
player.alpha = 1 - playerFadeProgress
|
||||
player.visibility = if (playerFadeProgress > 0.99f) View.INVISIBLE else View.VISIBLE
|
||||
player?.alpha = 1 - playerFadeProgress
|
||||
player?.visibility = if (playerFadeProgress > 0.99f) View.INVISIBLE else View.VISIBLE
|
||||
val toolbarFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.6f).toDouble())) / 0.2f).toFloat()
|
||||
toolbar.setAlpha(toolbarFadeProgress)
|
||||
toolbar.visibility = if (toolbarFadeProgress < 0.01f) View.INVISIBLE else View.VISIBLE
|
||||
|
@ -38,6 +38,7 @@ import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.util.Log
|
||||
@ -85,6 +86,8 @@ import java.util.concurrent.Semaphore
|
||||
private var feed: Feed? = null
|
||||
private var episodes: MutableList<Episode> = mutableListOf()
|
||||
|
||||
private var enableFilter: Boolean = true
|
||||
|
||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -337,13 +340,17 @@ import java.util.concurrent.Semaphore
|
||||
adapter.updateItems(episodes)
|
||||
}
|
||||
FlowEvent.EpisodesFilterOrSortEvent.Action.FILTER_CHANGED -> {
|
||||
feed!!.preferences?.filterString = event.feed.preferences?.filterString ?: ""
|
||||
val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) }
|
||||
episodes.clear()
|
||||
episodes.addAll(episodes_)
|
||||
if (enableFilter) {
|
||||
feed!!.preferences?.filterString = event.feed.preferences?.filterString ?: ""
|
||||
val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) }
|
||||
episodes.addAll(episodes_)
|
||||
} else {
|
||||
episodes.addAll(feed!!.episodes)
|
||||
}
|
||||
val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0)
|
||||
if (sortOrder != null) getPermutor(sortOrder).reorder(episodes)
|
||||
binding.header.counts.text = episodes_.size.toString()
|
||||
binding.header.counts.text = episodes.size.toString()
|
||||
adapter.updateItems(episodes)
|
||||
}
|
||||
}
|
||||
@ -596,12 +603,22 @@ import java.util.concurrent.Semaphore
|
||||
}
|
||||
}
|
||||
binding.header.butFilter.setOnClickListener {
|
||||
if (feed != null) {
|
||||
if (enableFilter && feed != null) {
|
||||
val dialog = FeedEpisodeFilterDialog(feed)
|
||||
dialog.filter = feed!!.episodeFilter
|
||||
dialog.show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
binding.header.butFilter.setOnLongClickListener {
|
||||
if (feed != null) {
|
||||
enableFilter = !enableFilter
|
||||
if (enableFilter) binding.header.butFilter.setColorFilter(Color.WHITE)
|
||||
else binding.header.butFilter.setColorFilter(Color.RED)
|
||||
onEpisodesFilterSortEvent(FlowEvent.EpisodesFilterOrSortEvent(FlowEvent.EpisodesFilterOrSortEvent.Action.FILTER_CHANGED, feed!!))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
binding.header.txtvFailure.setOnClickListener { showErrorDetails() }
|
||||
binding.header.counts.text = adapter.itemCount.toString()
|
||||
headerCreated = true
|
||||
|
@ -11,10 +11,8 @@ import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
|
||||
import ac.mdiq.podcini.storage.database.Feeds.feeds
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.model.DatasetStats
|
||||
import ac.mdiq.podcini.storage.model.PlayQueue
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeFilter.Companion.unfiltered
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
@ -55,7 +53,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import io.realm.kotlin.query.Sort
|
||||
import kotlinx.coroutines.*
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@ -408,7 +405,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
||||
Logd(TAG, "getNavDrawerData() called")
|
||||
val numDownloadedItems = getEpisodesCount(EpisodeFilter(EpisodeFilter.DOWNLOADED))
|
||||
val numItems = getEpisodesCount(unfiltered())
|
||||
val numFeeds = feeds.size
|
||||
val numFeeds = getFeedList().size
|
||||
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
|
||||
val queueSize = curQueue.episodeIds.size
|
||||
// if (queueSize == 0) {
|
||||
|
@ -132,7 +132,7 @@ import java.util.*
|
||||
binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
|
||||
adapter = QueueRecyclerAdapter(activity as MainActivity, swipeActions)
|
||||
adapter = QueueRecyclerAdapter()
|
||||
adapter?.setOnSelectModeListener(this)
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
@ -260,7 +260,7 @@ import java.util.*
|
||||
for (e in event.episodes) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
|
||||
if (pos >= 0) {
|
||||
Logd(TAG, "removing episode $pos ${queueItems[pos]} ${e}")
|
||||
Logd(TAG, "removing episode $pos ${queueItems[pos]} $e")
|
||||
queueItems.removeAt(pos)
|
||||
adapter?.notifyItemRemoved(pos)
|
||||
} else {
|
||||
@ -661,7 +661,7 @@ import java.util.*
|
||||
}
|
||||
}
|
||||
|
||||
private inner class QueueRecyclerAdapter(mainActivity: MainActivity, private val swipeActions: SwipeActions) : EpisodesAdapter(mainActivity) {
|
||||
private inner class QueueRecyclerAdapter : EpisodesAdapter(activity as MainActivity) {
|
||||
private var dragDropEnabled: Boolean
|
||||
|
||||
init {
|
||||
|
@ -3,11 +3,14 @@ package ac.mdiq.podcini.ui.fragment
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.*
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.feedOrder
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getTags
|
||||
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
|
||||
import ac.mdiq.podcini.storage.database.Feeds.sortFeeds
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedMap
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.model.FeedPreferences
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.FeedMenuHandler
|
||||
@ -33,7 +36,6 @@ import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.cardview.widget.CardView
|
||||
@ -51,12 +53,12 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.ref.WeakReference
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
@ -69,7 +71,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var subscriptionRecycler: RecyclerView
|
||||
private lateinit var subscriptionAdapter: SubscriptionsAdapter
|
||||
private lateinit var listAdapter: ListAdapter
|
||||
private lateinit var emptyView: EmptyViewHandler
|
||||
private lateinit var feedsInfoMsg: LinearLayout
|
||||
private lateinit var feedsFilteredMsg: TextView
|
||||
@ -83,7 +85,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
private var displayedFolder: String = ""
|
||||
private var displayUpArrow = false
|
||||
|
||||
private var feedList: List<Feed> = mutableListOf()
|
||||
private var feedList: MutableList<Feed> = mutableListOf()
|
||||
private var feedListFiltered: List<Feed> = mutableListOf()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -125,12 +127,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
subscriptionAdapter = SubscriptionsAdapter(activity as MainActivity)
|
||||
listAdapter = ListAdapter()
|
||||
val gridLayoutManager = GridLayoutManager(context, 1, RecyclerView.VERTICAL, false)
|
||||
subscriptionRecycler.layoutManager = gridLayoutManager
|
||||
|
||||
subscriptionAdapter.setOnSelectModeListener(this)
|
||||
subscriptionRecycler.adapter = subscriptionAdapter
|
||||
listAdapter.setOnSelectModeListener(this)
|
||||
subscriptionRecycler.adapter = listAdapter
|
||||
setupEmptyView()
|
||||
|
||||
resetTags()
|
||||
@ -155,7 +157,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
val resultList = feedListFiltered.filter {
|
||||
it.title?.lowercase(Locale.getDefault())?.contains(text)?:false || it.author?.lowercase(Locale.getDefault())?.contains(text)?:false
|
||||
}
|
||||
subscriptionAdapter.setItems(resultList)
|
||||
listAdapter.setItems(resultList)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
@ -195,7 +197,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
override fun onToggleChanged(isOpen: Boolean) {}
|
||||
})
|
||||
speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
|
||||
FeedMultiSelectActionHandler(activity as MainActivity, subscriptionAdapter.selectedItems.filterIsInstance<Feed>()).handleAction(actionItem.id)
|
||||
FeedMultiSelectActionHandler(activity as MainActivity, listAdapter.selectedItems.filterIsInstance<Feed>()).handleAction(actionItem.id)
|
||||
true
|
||||
}
|
||||
|
||||
@ -211,7 +213,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
subscriptionAdapter.endSelectMode()
|
||||
listAdapter.endSelectMode()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
|
||||
@ -240,7 +242,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
}
|
||||
}
|
||||
feedCount.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
subscriptionAdapter.setItems(feedListFiltered)
|
||||
listAdapter.setItems(feedListFiltered)
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
@ -300,16 +302,16 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
sortFeeds()
|
||||
val feeds: List<Feed> = getFeedList()
|
||||
feeds
|
||||
val fList: List<Feed> = getFeedList()
|
||||
fList
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
// We have fewer items. This can result in items being selected that are no longer visible.
|
||||
if ( feedListFiltered.size > result.size) subscriptionAdapter.endSelectMode()
|
||||
feedList = result
|
||||
if ( feedListFiltered.size > result.size) listAdapter.endSelectMode()
|
||||
feedList = result.toMutableList()
|
||||
filterOnTag()
|
||||
progressBar.visibility = View.GONE
|
||||
subscriptionAdapter.setItems(feedListFiltered)
|
||||
listAdapter.setItems(feedListFiltered)
|
||||
feedCount.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
emptyView.updateVisibility()
|
||||
}
|
||||
@ -322,26 +324,116 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
// else feedsFilteredMsg.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun sortFeeds() {
|
||||
Logd(TAG, "sortFeeds() called")
|
||||
val feedOrder = feedOrder
|
||||
val comparator: Comparator<Feed> = when (feedOrder) {
|
||||
UserPreferences.FEED_ORDER_UNPLAYED -> {
|
||||
val episodes = realm.query(Episode::class).query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_ALPHABETICAL -> {
|
||||
Comparator { lhs: Feed, rhs: Feed ->
|
||||
val t1 = lhs.title
|
||||
val t2 = rhs.title
|
||||
when {
|
||||
t1 == null -> 1
|
||||
t2 == null -> -1
|
||||
else -> t1.compareTo(t2, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPreferences.FEED_ORDER_MOST_PLAYED -> {
|
||||
val episodes = realm.query(Episode::class).query("playState == ${Episode.PLAYED}").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_LAST_UPDATED -> {
|
||||
val episodes = realm.query(Episode::class).find()
|
||||
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
|
||||
}
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_LAST_UNREAD_UPDATED -> {
|
||||
val episodes = realm.query(Episode::class)
|
||||
.query("playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}").find()
|
||||
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
|
||||
}
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_DOWNLOADED -> {
|
||||
val episodes = realm.query(Episode::class).query("media.downloaded == 1").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
UserPreferences.FEED_ORDER_DOWNLOADED_UNPLAYED -> {
|
||||
val episodes = realm.query(Episode::class)
|
||||
.query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == 1").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
// doing FEED_ORDER_NEW
|
||||
else -> {
|
||||
val episodes = realm.query(Episode::class).query("playState == ${Episode.NEW}").find()
|
||||
val counterMap = counterMap(episodes)
|
||||
comparator(counterMap)
|
||||
}
|
||||
}
|
||||
feedList.sortWith(comparator)
|
||||
}
|
||||
|
||||
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>): Comparator<Feed> {
|
||||
return Comparator { lhs: Feed, rhs: Feed ->
|
||||
val counterLhs = counterMap[lhs.id]?:0
|
||||
val counterRhs = counterMap[rhs.id]?:0
|
||||
when {
|
||||
// reverse natural order: podcast with most unplayed episodes first
|
||||
counterLhs > counterRhs -> -1
|
||||
counterLhs == counterRhs -> lhs.title?.compareTo(rhs.title!!, ignoreCase = true) ?: -1
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
val feed: Feed = subscriptionAdapter.getSelectedItem() ?: return false
|
||||
val feed: Feed = listAdapter.getSelectedItem() ?: return false
|
||||
val itemId = item.itemId
|
||||
if (itemId == R.id.multi_select) {
|
||||
speedDialView.visibility = View.VISIBLE
|
||||
return subscriptionAdapter.onContextItemSelected(item)
|
||||
return listAdapter.onContextItemSelected(item)
|
||||
}
|
||||
// TODO: this appears not called
|
||||
return FeedMenuHandler.onMenuItemClicked(this, item.itemId, feed) { this.loadSubscriptions() }
|
||||
}
|
||||
|
||||
private fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent?) {
|
||||
updateFeedList()
|
||||
updateFeedMap()
|
||||
loadSubscriptions()
|
||||
}
|
||||
|
||||
override fun onEndSelectMode() {
|
||||
speedDialView.close()
|
||||
speedDialView.visibility = View.GONE
|
||||
subscriptionAdapter.setItems(feedListFiltered)
|
||||
listAdapter.setItems(feedListFiltered)
|
||||
}
|
||||
|
||||
override fun onStartSelectMode() {
|
||||
@ -350,12 +442,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
for (item in feedListFiltered) {
|
||||
feedsOnly.add(item)
|
||||
}
|
||||
subscriptionAdapter.setItems(feedsOnly)
|
||||
listAdapter.setItems(feedsOnly)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
class FeedMultiSelectActionHandler(private val activity: MainActivity, private val selectedItems: List<Feed>) {
|
||||
|
||||
fun handleAction(id: Int) {
|
||||
when (id) {
|
||||
R.id.remove_feed -> RemoveFeedDialog.show(activity, selectedItems)
|
||||
@ -366,26 +457,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
R.id.autodownload -> autoDownloadPrefHandler()
|
||||
R.id.autoDeleteDownload -> autoDeleteEpisodesPrefHandler()
|
||||
R.id.playback_speed -> playbackSpeedPrefHandler()
|
||||
R.id.edit_tags -> editFeedPrefTags()
|
||||
R.id.edit_tags -> TagSettingsDialog.newInstance(selectedItems).show(activity.supportFragmentManager, TAG)
|
||||
else -> Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=$id")
|
||||
}
|
||||
}
|
||||
|
||||
// private fun notifyNewEpisodesPrefHandler() {
|
||||
// val preferenceSwitchDialog = PreferenceSwitchDialog(activity,
|
||||
// activity.getString(R.string.episode_notification),
|
||||
// activity.getString(R.string.episode_notification_summary))
|
||||
//
|
||||
// preferenceSwitchDialog.setOnPreferenceChangedListener(object: PreferenceSwitchDialog.OnPreferenceChangedListener {
|
||||
// @UnstableApi override fun preferenceChanged(enabled: Boolean) {
|
||||
// saveFeedPreferences { feedPreferences: FeedPreferences ->
|
||||
// feedPreferences.showEpisodeNotification = enabled
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// preferenceSwitchDialog.openDialog()
|
||||
// }
|
||||
|
||||
private fun autoDownloadPrefHandler() {
|
||||
val preferenceSwitchDialog = PreferenceSwitchDialog(activity, activity.getString(R.string.auto_download_settings_label), activity.getString(R.string.auto_download_label))
|
||||
preferenceSwitchDialog.setOnPreferenceChangedListener(@UnstableApi object: PreferenceSwitchDialog.OnPreferenceChangedListener {
|
||||
@ -395,7 +470,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
})
|
||||
preferenceSwitchDialog.openDialog()
|
||||
}
|
||||
|
||||
@UnstableApi private fun playbackSpeedPrefHandler() {
|
||||
val viewBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater)
|
||||
viewBinding.seekBar.setProgressChangedListener { speed: Float? ->
|
||||
@ -420,7 +494,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun autoDeleteEpisodesPrefHandler() {
|
||||
val preferenceListDialog = PreferenceListDialog(activity, activity.getString(R.string.auto_delete_label))
|
||||
val items: Array<String> = activity.resources.getStringArray(R.array.spnAutoDeleteItems)
|
||||
@ -434,7 +507,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun keepUpdatedPrefHandler() {
|
||||
val preferenceSwitchDialog = PreferenceSwitchDialog(activity, activity.getString(R.string.kept_updated), activity.getString(R.string.keep_updated_summary))
|
||||
preferenceSwitchDialog.setOnPreferenceChangedListener(object: PreferenceSwitchDialog.OnPreferenceChangedListener {
|
||||
@ -446,11 +518,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
})
|
||||
preferenceSwitchDialog.openDialog()
|
||||
}
|
||||
|
||||
@UnstableApi private fun showMessage(@PluralsRes msgId: Int, numItems: Int) {
|
||||
activity.showSnackbarAbovePlayer(activity.resources.getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG)
|
||||
}
|
||||
|
||||
@UnstableApi private fun saveFeedPreferences(preferencesConsumer: Consumer<FeedPreferences>) {
|
||||
for (feed in selectedItems) {
|
||||
if (feed.preferences == null) continue
|
||||
@ -458,22 +525,18 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
persistFeedPreferences(feed)
|
||||
// EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed.preferences!!.feedID))
|
||||
}
|
||||
showMessage(R.plurals.updated_feeds_batch_label, selectedItems.size)
|
||||
val numItems = selectedItems.size
|
||||
activity.showSnackbarAbovePlayer(activity.resources.getQuantityString(R.plurals.updated_feeds_batch_label, numItems, numItems), Snackbar.LENGTH_LONG)
|
||||
}
|
||||
|
||||
private fun editFeedPrefTags() {
|
||||
TagSettingsDialog.newInstance(selectedItems).show(activity.supportFragmentManager, TAG)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = FeedMultiSelectActionHandler::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SubscriptionsAdapter(mainActivity: MainActivity)
|
||||
: SelectableAdapter<SubscriptionsAdapter.SubscriptionViewHolder?>(mainActivity), View.OnCreateContextMenuListener {
|
||||
@OptIn(UnstableApi::class)
|
||||
private inner class ListAdapter
|
||||
: SelectableAdapter<ListAdapter.ViewHolder?>(activity as MainActivity), View.OnCreateContextMenuListener {
|
||||
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private var feedList: List<Feed>
|
||||
private var selectedItem: Feed? = null
|
||||
private var longPressedPosition: Int = 0 // used to init actionMode
|
||||
@ -499,11 +562,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
fun getSelectedItem(): Feed? {
|
||||
return selectedItem
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
|
||||
val itemView: View = LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false)
|
||||
return SubscriptionViewHolder(itemView)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val itemView: View = LayoutInflater.from(activity).inflate(R.layout.subscription_item, parent, false)
|
||||
return ViewHolder(itemView)
|
||||
}
|
||||
@UnstableApi override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
|
||||
@UnstableApi override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val feed: Feed = feedList[position]
|
||||
holder.bind(feed)
|
||||
if (inActionMode()) {
|
||||
@ -524,14 +587,14 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
if (inActionMode()) holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition))
|
||||
else {
|
||||
val fragment: Fragment = FeedInfoFragment.newInstance(feed)
|
||||
mainActivityRef.get()?.loadChildFragment(fragment)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
holder.infoCard.setOnClickListener {
|
||||
if (inActionMode()) holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition))
|
||||
else {
|
||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||
mainActivityRef.get()?.loadChildFragment(fragment)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
// holder.infoCard.setOnCreateContextMenuListener(this)
|
||||
@ -565,9 +628,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
if (position >= feedList.size) return RecyclerView.NO_ID // Dummy views
|
||||
return feedList[position].id
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
if (selectedItem == null) return
|
||||
val mainActRef = mainActivityRef.get() ?: return
|
||||
val mainActRef = (activity as MainActivity)
|
||||
val inflater: MenuInflater = mainActRef.menuInflater
|
||||
if (inActionMode()) {
|
||||
// inflater.inflate(R.menu.multi_select_context_popup, menu)
|
||||
@ -593,7 +657,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private inner class SubscriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding = SubscriptionItemBinding.bind(itemView)
|
||||
private val title = binding.titleLabel
|
||||
private val producer = binding.producerLabel
|
||||
@ -619,8 +683,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
count.text = NumberFormat.getInstance().format(counter.toLong()) + " episodes"
|
||||
count.visibility = View.VISIBLE
|
||||
|
||||
val mainActRef = mainActivityRef.get() ?: return
|
||||
|
||||
val mainActRef = (activity as MainActivity)
|
||||
val coverLoader = CoverLoader(mainActRef)
|
||||
val feed: Feed = drawerItem
|
||||
coverLoader.withUri(feed.imageUrl)
|
||||
|
@ -110,7 +110,7 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro
|
||||
container.alpha = if (item.isPlayed()) 0.75f else 1.0f
|
||||
|
||||
val newButton = EpisodeActionButton.forItem(item)
|
||||
Logd(TAG, "Trying to bind button ${actionButton?.TAG} ${newButton.TAG} ${item.title}")
|
||||
// Logd(TAG, "Trying to bind button ${actionButton?.TAG} ${newButton.TAG} ${item.title}")
|
||||
// not using a new button to ensure valid progress values, for TTS audio generation
|
||||
if (!(actionButton?.TAG == TTSActionButton::class.simpleName && newButton.TAG == TTSActionButton::class.simpleName)) {
|
||||
actionButton = newButton
|
||||
|
@ -127,7 +127,7 @@ sealed class FlowEvent {
|
||||
data class FeedListUpdateEvent(val feedIds: List<Long> = emptyList()) : FlowEvent() {
|
||||
constructor(feed: Feed) : this(listOf(feed.id))
|
||||
constructor(feedId: Long) : this(listOf(feedId))
|
||||
constructor(feeds: List<Feed>, junkInfo: String = "") : this(feeds.map { it.id })
|
||||
constructor(feeds: List<Feed>, junk: String = "") : this(feeds.map { it.id })
|
||||
|
||||
fun contains(feed: Feed): Boolean {
|
||||
return feedIds.contains(feed.id)
|
||||
|
10
changelog.md
10
changelog.md
@ -1,3 +1,13 @@
|
||||
## 6.0.1
|
||||
|
||||
* removing a list of feeds is speedier
|
||||
* fixed issue of not starting the next in queue
|
||||
* queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
|
||||
* fixed crash issue in AudioPlayer due to view uninitialized
|
||||
* improved efficiency of getFeed
|
||||
* Tapping the media playback notification opens Podcini
|
||||
* Long-press filter button in FeedEpisode view enables/disables filters without changing filter settings
|
||||
|
||||
## 6.0.0
|
||||
|
||||
* complete overhaul of database and routines, ditched the iron-age celebrity SQLite and entrusted the modern object-based Realm
|
||||
|
10
fastlane/metadata/android/en-US/changelogs/3020201.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/3020201.txt
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
Version 6.0.1 brings several changes:
|
||||
|
||||
* removing a list of feeds is speedier
|
||||
* fixed issue of not starting the next in queue
|
||||
* queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
|
||||
* fixed crash issue in AudioPlayer due to view uninitialized
|
||||
* improved efficiency of getFeed
|
||||
* Tapping the media playback notification opens Podcini
|
||||
* Long-press filter button in FeedEpisode view enables/disables filters without changing filter settings
|
Loading…
x
Reference in New Issue
Block a user