6.0.1 commit

This commit is contained in:
Xilin Jia 2024-06-24 21:52:25 +01:00
parent 2a66e7fb5d
commit 393063ad09
23 changed files with 283 additions and 279 deletions

View File

@ -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)

View File

@ -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 = ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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?:""
}
}

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View 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