6.3.4 commit

This commit is contained in:
Xilin Jia 2024-08-04 13:04:10 +01:00
parent e5188bc998
commit 5fe3f049d2
47 changed files with 382 additions and 284 deletions

View File

@ -22,7 +22,7 @@ Compared to AntennaPod this project:
3. Iron-age celebrity SQLite is replaced with modern object-base Realm DB (Podcini.R), 3. Iron-age celebrity SQLite is replaced with modern object-base Realm DB (Podcini.R),
4. Outfits with Viewbinding, Coil replacing Glide, coroutines replacing RxJava and threads, SharedFlow replacing EventBus, and jetifier removed, 4. Outfits with Viewbinding, Coil replacing Glide, coroutines replacing RxJava and threads, SharedFlow replacing EventBus, and jetifier removed,
5. Boasts new UI's including streamlined drawer, subscriptions view and player controller, 5. Boasts new UI's including streamlined drawer, subscriptions view and player controller,
6. Supports multiple and circular play queues associable to any podcast 6. Supports multiple, virtual and circular play queues associable to any podcast
7. Auto-download is governed by policy and limit settings of individual feed 7. Auto-download is governed by policy and limit settings of individual feed
8. Accepts podcast as well as plain RSS and YouTube feeds, 8. Accepts podcast as well as plain RSS and YouTube feeds,
9. Offers Readability and Text-to-Speech for RSS contents, 9. Offers Readability and Text-to-Speech for RSS contents,
@ -59,13 +59,20 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* easy switches on video player to other video mode or audio only * easy switches on video player to other video mode or audio only
* default video player mode setting in preferences * default video player mode setting in preferences
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view * when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
* "Prefer streaming over download" is now on setting of individual feed
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues * Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
* on app startup, the most recently updated queue is set to curQueue * on app startup, the most recently updated queue is set to curQueue
* any episodes can be easily added/moved to the active or any designated queues * any episodes can be easily added/moved to the active or any designated queues
* any queue can be associated with any feed for customized playing experience * any queue can be associated with any feed for customized playing experience
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played * Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
* Every queue has a bin containing past episodes removed from the queue * Every queue has a bin containing past episodes removed from the queue
* Episode played from a list other than the queue is now a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played * Feed associated queue can be set to None, in which case:
* episodes in the feed are not automatically added to any queue, but are used as a natural queue for getting the next episode to play
* the next episode is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
* else if "prefer streaming" is set, it's the next unplayed episode in the feed episodes list based on the current sort order
* else it's the next downloaded unplayed episode
* Otherwise, episode played from a list other than the queue is now a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played
### Podcast/Episode list ### Podcast/Episode list

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020227 versionCode 3020228
versionName "6.3.3" versionName "6.3.4"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -105,7 +105,7 @@ class MediaPlayerBaseTest {
VolumeAdaptionSetting.OFF, null, null) VolumeAdaptionSetting.OFF, null, null)
f.preferences = prefs f.preferences = prefs
f.episodes.clear() f.episodes.clear()
val i = Episode(0, "t", "i", "l", Date(), Episode.UNPLAYED, f) val i = Episode(0, "t", "i", "l", Date(), Episode.PlayState.UNPLAYED.code, f)
f.episodes.add(i) f.episodes.add(i)
val media = EpisodeMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0, 0) val media = EpisodeMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0, 0)
i.setMedia(media) i.setMedia(media)

View File

@ -65,7 +65,7 @@ class TaskManagerTest {
val f = Feed(0, null, "title", "link", "d", null, null, null, null, "id", null, "null", "url") val f = Feed(0, null, "title", "link", "d", null, null, null, null, "id", null, "null", "url")
f.episodes.clear() f.episodes.clear()
for (i in 0 until NUM_ITEMS) { for (i in 0 until NUM_ITEMS) {
f.episodes.add(Episode(0, pref + i, pref + i, "link", Date(), Episode.PLAYED, f)) f.episodes.add(Episode(0, pref + i, pref + i, "link", Date(), Episode.PlayState.PLAYED.code, f))
} }
// val adapter = getInstance() // val adapter = getInstance()
// adapter.open() // adapter.open()

View File

@ -113,7 +113,7 @@ class UITestUtils(private val context: Context) {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
for (j in 0 until NUM_ITEMS_PER_FEED) { for (j in 0 until NUM_ITEMS_PER_FEED) {
val item = Episode(j.toLong(), "Feed " + (i + 1) + ": Item " + (j + 1), "item$j", val item = Episode(j.toLong(), "Feed " + (i + 1) + ": Item " + (j + 1), "item$j",
"http://example.com/feed$i/item/$j", Date(), Episode.UNPLAYED, feed) "http://example.com/feed$i/item/$j", Date(), Episode.PlayState.UNPLAYED.code, feed)
items.add(item) items.add(item)
if (!hostTextOnlyFeeds) { if (!hostTextOnlyFeeds) {

View File

@ -14,7 +14,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.LogsAndStats import ac.mdiq.podcini.storage.database.LogsAndStats
import ac.mdiq.podcini.storage.database.Queues import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.DownloadResult import ac.mdiq.podcini.storage.model.DownloadResult
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
@ -78,7 +77,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Logd(TAG, "starting cancel") Logd(TAG, "starting cancel")
// This needs to be done here, not in the worker. Reason: The worker might or might not be running. // This needs to be done here, not in the worker. Reason: The worker might or might not be running.
val item_ = media.episodeOrFetch() val item_ = media.episodeOrFetch()
if (item_ != null) Episodes.deleteMediaOfEpisode(context, item_) // Remove partially downloaded file if (item_ != null) Episodes.deleteEpisodeMedia(context, item_) // Remove partially downloaded file
val tag = WORK_TAG_EPISODE_URL + media.downloadUrl val tag = WORK_TAG_EPISODE_URL + media.downloadUrl
val future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag) val future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)
@ -124,7 +123,8 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
.addTag(WORK_TAG) .addTag(WORK_TAG)
.addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl) .addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl)
if (enqueueDownloadedEpisodes()) { if (enqueueDownloadedEpisodes()) {
runBlocking { Queues.addToQueueSync(false, item, item.feed?.preferences?.queue) } if (item.feed?.preferences?.queue != null)
runBlocking { Queues.addToQueueSync(false, item, item.feed?.preferences?.queue) }
workRequest.addTag(WORK_DATA_WAS_QUEUED) workRequest.addTag(WORK_DATA_WAS_QUEUED)
} }
workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build()) workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build())

View File

@ -132,7 +132,7 @@ object LocalFeedUpdater {
} }
private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode { private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode {
val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.UNPLAYED, feed) val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.PlayState.UNPLAYED.code, feed)
item.disableAutoDownload() item.disableAutoDownload()
val size = file.length val size = file.length

View File

@ -253,8 +253,11 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
curMedia = playable curMedia = playable
if (curMedia is EpisodeMedia) { if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia val media_ = curMedia as EpisodeMedia
curIndexInQueue = EpisodeUtil.indexOfItemWithId(curQueue.episodes, media_.id) val item = media_.episodeOrFetch()
val eList = if (item?.feed?.preferences?.queue != null) curQueue.episodes else item?.feed?.getVirtualQueueItems() ?: listOf()
curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id)
} else curIndexInQueue = -1 } else curIndexInQueue = -1
prevMedia = curMedia prevMedia = curMedia
this.isStreaming = stream this.isStreaming = stream
mediaType = curMedia!!.getMediaType() mediaType = curMedia!!.getMediaType()

View File

@ -311,7 +311,7 @@ class PlaybackService : MediaSessionService() {
if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode())) { if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode())) {
Logd(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped") Logd(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped")
// only mark the item as played if we're not keeping it anyways // only mark the item as played if we're not keeping it anyways
item = setPlayStateSync(Episode.PLAYED, ended || (skipped && smartMarkAsPlayed), item!!) item = setPlayStateSync(Episode.PlayState.PLAYED.code, ended || (skipped && smartMarkAsPlayed), item!!)
val action = item?.feed?.preferences?.autoDeleteAction val action = item?.feed?.preferences?.autoDeleteAction
val shouldAutoDelete = (action == AutoDeleteAction.ALWAYS || val shouldAutoDelete = (action == AutoDeleteAction.ALWAYS ||
(action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!))) (action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!)))
@ -355,11 +355,6 @@ class PlaybackService : MediaSessionService() {
override fun getNextInQueue(currentMedia: Playable?): Playable? { override fun getNextInQueue(currentMedia: Playable?): Playable? {
Logd(TAG, "call getNextInQueue currentMedia: ${currentMedia?.getEpisodeTitle()}") Logd(TAG, "call getNextInQueue currentMedia: ${currentMedia?.getEpisodeTitle()}")
if (curIndexInQueue < 0) {
Logd(TAG, "getNextInQueue(), curMedia is not in curQueue")
writeNoMediaPlaying()
return null
}
if (currentMedia !is EpisodeMedia) { if (currentMedia !is EpisodeMedia) {
Logd(TAG, "getNextInQueue(), but playable not an instance of EpisodeMedia, so not proceeding") Logd(TAG, "getNextInQueue(), but playable not an instance of EpisodeMedia, so not proceeding")
writeNoMediaPlaying() writeNoMediaPlaying()
@ -371,20 +366,29 @@ class PlaybackService : MediaSessionService() {
writeNoMediaPlaying() writeNoMediaPlaying()
return null return null
} }
// val nextItem = getNextInQueue(item) if (curIndexInQueue < 0 && item.feed?.preferences?.queue != null) {
if (curQueue.episodes.isEmpty()) { Logd(TAG, "getNextInQueue(), curMedia is not in curQueue")
writeNoMediaPlaying()
return null
}
val eList = if (item.feed?.preferences?.queue == null) item.feed?.getVirtualQueueItems() else curQueue.episodes
if (eList.isNullOrEmpty()) {
Logd(TAG, "getNextInQueue queue is empty") Logd(TAG, "getNextInQueue queue is empty")
writeNoMediaPlaying() writeNoMediaPlaying()
return null return null
} }
Logd(TAG, "getNextInQueue eList: ${eList.size}")
var j = 0 var j = 0
val i = EpisodeUtil.indexOfItemWithId(curQueue.episodes, item.id) val i = EpisodeUtil.indexOfItemWithId(eList, item.id)
Logd(TAG, "getNextInQueue current i: $i curIndexInQueue: $curIndexInQueue")
if (i < 0) { if (i < 0) {
if (curIndexInQueue < curQueue.episodes.size) j = curIndexInQueue if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) j = curIndexInQueue
else j = curQueue.episodes.size-1 else j = eList.size-1
} else if (i < curQueue.episodes.size-1) j = i+1 } else if (i < eList.size-1) j = i+1
Logd(TAG, "getNextInQueue next j: $j")
val nextItem = unmanaged(curQueue.episodes[j]) val nextItem = unmanaged(eList[j])
Logd(TAG, "getNextInQueue nextItem ${nextItem.title}")
if (nextItem.media == null) { if (nextItem.media == null) {
Logd(TAG, "getNextInQueue nextItem: $nextItem media is null") Logd(TAG, "getNextInQueue nextItem: $nextItem media is null")
writeNoMediaPlaying() writeNoMediaPlaying()
@ -397,7 +401,7 @@ class PlaybackService : MediaSessionService() {
return null return null
} }
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) { if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed?.isLocalFeed != true) {
Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}") Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}")
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent) displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent)
writeNoMediaPlaying() writeNoMediaPlaying()
@ -405,7 +409,7 @@ class PlaybackService : MediaSessionService() {
} }
EventFlow.postEvent(FlowEvent.PlayEvent(item, FlowEvent.PlayEvent.Action.END)) EventFlow.postEvent(FlowEvent.PlayEvent(item, FlowEvent.PlayEvent.Action.END))
EventFlow.postEvent(FlowEvent.PlayEvent(nextItem)) EventFlow.postEvent(FlowEvent.PlayEvent(nextItem))
return if (nextItem.media == null) nextItem.media else unmanaged(nextItem.media!!) return if (nextItem.media == null) null else unmanaged(nextItem.media!!)
} }
override fun findMedia(url: String): Playable? { override fun findMedia(url: String): Playable? {
@ -419,14 +423,12 @@ class PlaybackService : MediaSessionService() {
if (stopPlaying) taskManager.cancelPositionSaver() if (stopPlaying) taskManager.cancelPositionSaver()
if (mediaType == null) sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0) if (mediaType == null) sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0)
else { else sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, when {
when { isCasting -> EXTRA_CODE_CAST
isCasting -> EXTRA_CODE_CAST mediaType == MediaType.VIDEO -> EXTRA_CODE_VIDEO
mediaType == MediaType.VIDEO -> EXTRA_CODE_VIDEO else -> EXTRA_CODE_AUDIO
else -> EXTRA_CODE_AUDIO })
})
}
} }
override fun ensureMediaInfoLoaded(media: Playable) { override fun ensureMediaInfoLoaded(media: Playable) {
@ -962,7 +964,7 @@ class PlaybackService : MediaSessionService() {
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) { if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}") Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
for (e in event.episodes) { for (e in event.episodes) {
Logd(TAG, "onQueueEvent: ending playback event ${e?.title}") Logd(TAG, "onQueueEvent: ending playback event ${e.title}")
if (e.id == curEpisode?.id) { if (e.id == curEpisode?.id) {
mPlayer?.endPlayback(hasEnded = false, wasSkipped = true, shouldContinue = true, toStoppedState = true) mPlayer?.endPlayback(hasEnded = false, wasSkipped = true, shouldContinue = true, toStoppedState = true)
break break
@ -1075,7 +1077,7 @@ class PlaybackService : MediaSessionService() {
if (media != null) { if (media != null) {
media.setPosition(position) media.setPosition(position)
media.setLastPlayedTime(System.currentTimeMillis()) media.setLastPlayedTime(System.currentTimeMillis())
if (it.isNew) it.playState = Episode.UNPLAYED if (it.isNew) it.playState = Episode.PlayState.UNPLAYED.code
if (media.startPosition >= 0 && media.getPosition() > media.startPosition) if (media.startPosition >= 0 && media.getPosition() > media.startPosition)
media.playedDuration = (media.playedDurationWhenStarted + media.getPosition() - media.startPosition) media.playedDuration = (media.playedDurationWhenStarted + media.getPosition() - media.startPosition)
} }

View File

@ -5,7 +5,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.EPISODE_CLEANUP_NULL
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.database.Queues.getInQueueEpisodeIds import ac.mdiq.podcini.storage.database.Queues.getInQueueEpisodeIds
@ -84,7 +84,7 @@ object AutoCleanups {
for (item in delete) { for (item in delete) {
if (item.media == null) continue if (item.media == null) continue
try { try {
runBlocking { deleteMediaOfEpisode(context, item).join() } runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
e.printStackTrace() e.printStackTrace()
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
@ -138,7 +138,7 @@ object AutoCleanups {
for (item in delete) { for (item in delete) {
if (item.media == null) continue if (item.media == null) continue
try { try {
runBlocking { deleteMediaOfEpisode(context, item).join() } runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
e.printStackTrace() e.printStackTrace()
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
@ -205,7 +205,7 @@ object AutoCleanups {
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
for (item in delete) { for (item in delete) {
try { try {
runBlocking { deleteMediaOfEpisode(context, item).join() } runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
e.printStackTrace() e.printStackTrace()
} catch (e: ExecutionException) { } catch (e: ExecutionException) {

View File

@ -19,10 +19,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Episode.Companion.BUILDING import ac.mdiq.podcini.storage.model.Episode.PlayState
import ac.mdiq.podcini.storage.model.Episode.Companion.NEW
import ac.mdiq.podcini.storage.model.Episode.Companion.PLAYED
import ac.mdiq.podcini.storage.model.Episode.Companion.UNPLAYED
import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.model.EpisodeSortOrder
@ -98,7 +95,7 @@ object Episodes {
// @JvmStatic is needed because some Runnable blocks call this // @JvmStatic is needed because some Runnable blocks call this
@OptIn(UnstableApi::class) @JvmStatic @OptIn(UnstableApi::class) @JvmStatic
fun deleteMediaOfEpisode(context: Context, episode: Episode) : Job { fun deleteEpisodeMedia(context: Context, episode: Episode) : Job {
Logd(TAG, "deleteMediaOfEpisode called ${episode.title}") Logd(TAG, "deleteMediaOfEpisode called ${episode.title}")
return runOnIOScope { return runOnIOScope {
if (episode.media == null) return@runOnIOScope if (episode.media == null) return@runOnIOScope
@ -136,7 +133,7 @@ object Episodes {
url != null -> { url != null -> {
// delete downloaded media file // delete downloaded media file
val mediaFile = File(url) val mediaFile = File(url)
if (mediaFile.exists() && !mediaFile.delete()) { if (!mediaFile.delete()) {
Log.e(TAG, "delete media file failed: $url") Log.e(TAG, "delete media file failed: $url")
val evt = FlowEvent.MessageEvent(context.getString(R.string.delete_failed)) val evt = FlowEvent.MessageEvent(context.getString(R.string.delete_failed))
EventFlow.postEvent(evt) EventFlow.postEvent(evt)
@ -176,6 +173,7 @@ object Episodes {
* Remove the listed episodes and their EpisodeMedia entries. * Remove the listed episodes and their EpisodeMedia entries.
* Deleting media also removes the download log entries. * Deleting media also removes the download log entries.
*/ */
@UnstableApi
fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job { fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job {
return runOnIOScope { return runOnIOScope {
val removedFromQueue: MutableList<Episode> = ArrayList() val removedFromQueue: MutableList<Episode> = ArrayList()
@ -290,19 +288,19 @@ object Episodes {
suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode { suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode {
Logd(TAG, "setPlayStateSync called resetMediaPosition: $resetMediaPosition") Logd(TAG, "setPlayStateSync called resetMediaPosition: $resetMediaPosition")
val result = upsert(episode) { val result = upsert(episode) {
if (played >= NEW && played <= BUILDING) it.playState = played if (played >= PlayState.NEW.code && played <= PlayState.BUILDING.code) it.playState = played
else { else {
if (it.playState == PLAYED) it.playState = UNPLAYED if (it.playState == PlayState.PLAYED.code) it.playState = PlayState.UNPLAYED.code
else it.playState = PLAYED else it.playState = PlayState.PLAYED.code
} }
if (resetMediaPosition) it.media?.setPosition(0) if (resetMediaPosition) it.media?.setPosition(0)
} }
if (played == PLAYED && shouldRemoveFromQueuesMarkPlayed()) removeFromAllQueuesSync(result) if (played == PlayState.PLAYED.code && shouldMarkedPlayedRemoveFromQueues()) removeFromAllQueuesSync(result)
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result)) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result))
return result return result
} }
private fun shouldRemoveFromQueuesMarkPlayed(): Boolean { private fun shouldMarkedPlayedRemoveFromQueues(): Boolean {
return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true) return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
} }
} }

View File

@ -377,13 +377,6 @@ object Feeds {
backupManager.dataChanged() backupManager.dataChanged()
} }
// private fun persistFeedsSync(vararg feeds: Feed) {
// Logd(TAG, "persistFeedsSync called")
// for (feed in feeds) {
// upsertBlk(feed) {}
// }
// }
fun persistFeedPreferences(feed: Feed) : Job { fun persistFeedPreferences(feed: Feed) : Job {
Logd(TAG, "persistFeedPreferences called") Logd(TAG, "persistFeedPreferences called")
return runOnIOScope { return runOnIOScope {

View File

@ -56,7 +56,7 @@ object LogsAndStats {
feedTotalTime += m.duration feedTotalTime += m.duration
if (m.lastPlayedTime in timeFilterFrom..<timeFilterTo) { if (m.lastPlayedTime in timeFilterFrom..<timeFilterTo) {
if (includeMarkedAsPlayed) { if (includeMarkedAsPlayed) {
if ((m.playbackCompletionTime > 0 && m.playedDuration > 0) || m.episodeOrFetch()?.playState == Episode.PLAYED || m.position > 0) { if ((m.playbackCompletionTime > 0 && m.playedDuration > 0) || m.episodeOrFetch()?.playState == Episode.PlayState.PLAYED.code || m.position > 0) {
episodesStarted += 1 episodesStarted += 1
feedPlayedTime += m.duration feedPlayedTime += m.duration
} }

View File

@ -11,6 +11,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.EpisodeUtil.indexOfItemWithId
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
@ -144,7 +145,7 @@ object Queues {
for (event in events) EventFlow.postEvent(event) for (event in events) EventFlow.postEvent(event)
// EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(updatedItems)) // EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(updatedItems))
if (markAsUnplayed && markAsUnplayeds.size > 0) setPlayState(Episode.UNPLAYED, false, *markAsUnplayeds.toTypedArray()) if (markAsUnplayed && markAsUnplayeds.size > 0) setPlayState(Episode.PlayState.UNPLAYED.code, false, *markAsUnplayeds.toTypedArray())
// if (performAutoDownload) autodownloadEpisodeMedia(context) // if (performAutoDownload) autodownloadEpisodeMedia(context)
} }
} }
@ -153,7 +154,6 @@ object Queues {
suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode, queue_: PlayQueue? = null) { suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode, queue_: PlayQueue? = null) {
Logd(TAG, "addToQueueSync( ... ) called") Logd(TAG, "addToQueueSync( ... ) called")
// val queue = if (queue_ != null) unmanaged(queue_) else curQueue
val queue = queue_ ?: curQueue val queue = queue_ ?: curQueue
val currentlyPlaying = curMedia val currentlyPlaying = curMedia
val positionCalculator = EnqueuePositionCalculator(enqueueLocation) val positionCalculator = EnqueuePositionCalculator(enqueueLocation)
@ -166,11 +166,9 @@ object Queues {
insertPosition++ insertPosition++
it.update() it.update()
} }
// queueNew.episodes.addAll(queue.episodes)
// queueNew.episodes.add(insertPosition, episode)
if (queue.id == curQueue.id) curQueue = queueNew if (queue.id == curQueue.id) curQueue = queueNew
if (markAsUnplayed && episode.isNew) setPlayState(Episode.UNPLAYED, false, episode) if (markAsUnplayed && episode.isNew) setPlayState(Episode.PlayState.UNPLAYED.code, false, episode)
if (queue.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition)) if (queue.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition))
// if (performAutoDownload) autodownloadEpisodeMedia(context) // if (performAutoDownload) autodownloadEpisodeMedia(context)
} }
@ -246,9 +244,10 @@ object Queues {
val events: MutableList<FlowEvent.QueueEvent> = ArrayList() val events: MutableList<FlowEvent.QueueEvent> = ArrayList()
val indicesToRemove: MutableList<Int> = mutableListOf() val indicesToRemove: MutableList<Int> = mutableListOf()
val qItems = queue.episodes.toMutableList() val qItems = queue.episodes.toMutableList()
val eList = episodes.toList()
for (i in qItems.indices) { for (i in qItems.indices) {
val episode = qItems[i] val episode = qItems[i]
if (episodes.contains(episode)) { if (indexOfItemWithId(eList, episode.id) >= 0) {
Logd(TAG, "removing from queue: ${episode.id} ${episode.title}") Logd(TAG, "removing from queue: ${episode.id} ${episode.title}")
indicesToRemove.add(i) indicesToRemove.add(i)
if (queue.id == curQueue.id) events.add(FlowEvent.QueueEvent.removed(episode)) if (queue.id == curQueue.id) events.add(FlowEvent.QueueEvent.removed(episode))

View File

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

View File

@ -86,7 +86,7 @@ class Episode : RealmObject {
@Ignore @Ignore
val isNew: Boolean val isNew: Boolean
get() = playState == NEW get() = playState == PlayState.NEW.code
@Ignore @Ignore
val isInProgress: Boolean val isInProgress: Boolean
@ -123,7 +123,7 @@ class Episode : RealmObject {
} }
constructor() { constructor() {
this.playState = UNPLAYED this.playState = PlayState.UNPLAYED.code
} }
/** /**
@ -187,19 +187,19 @@ class Episode : RealmObject {
} }
fun setNew() { fun setNew() {
playState = NEW playState = PlayState.NEW.code
} }
fun isPlayed(): Boolean { fun isPlayed(): Boolean {
return playState == PLAYED return playState == PlayState.PLAYED.code
} }
fun setPlayed(played: Boolean) { fun setPlayed(played: Boolean) {
playState = if (played) PLAYED else UNPLAYED playState = if (played) PlayState.PLAYED.code else PlayState.UNPLAYED.code
} }
fun setBuilding() { fun setBuilding() {
playState = BUILDING playState = PlayState.BUILDING.code
} }
/** /**
@ -252,13 +252,15 @@ class Episode : RealmObject {
return (id xor (id ushr 32)).toInt() return (id xor (id ushr 32)).toInt()
} }
enum class PlayState(val code: Int) {
UNSPECIFIED(-2),
NEW(-1),
UNPLAYED(0),
PLAYED(1),
BUILDING(2),
ABANDONED(3)
}
companion object { companion object {
val TAG: String = Episode::class.simpleName ?: "Anonymous" val TAG: String = Episode::class.simpleName ?: "Anonymous"
const val UNSPECIFIED: Int = -2
const val NEW: Int = -1
const val UNPLAYED: Int = 0
const val PLAYED: Int = 1
const val BUILDING: Int = 2
} }
} }

View File

@ -3,6 +3,7 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.FeedFunding.Companion.extractPaymentLinks import ac.mdiq.podcini.storage.model.FeedFunding.Companion.extractPaymentLinks
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.ext.realmListOf
import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmList
import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmObject
@ -144,20 +145,7 @@ class Feed : RealmObject {
@Ignore @Ignore
val mostRecentItem: Episode? val mostRecentItem: Episode?
get() { get() = realm.query(Episode::class).query("feedId == $id SORT(pubDate DESC)").first().find()
// // we could sort, but we don't need to, a simple search is fine...
// var mostRecentDate = Date(0)
// var mostRecentItem: Episode? = null
// for (item in episodes) {
// val date = item.getPubDate()
// if (date != null && date.after(mostRecentDate)) {
// mostRecentDate = date
// mostRecentItem = item
// }
// }
// return mostRecentItem
return realm.query(Episode::class).query("feedId == $id SORT(pubDate DESC)").first().find()
}
@Ignore @Ignore
var title: String? var title: String?
@ -297,6 +285,14 @@ class Feed : RealmObject {
paymentLinks.add(funding) paymentLinks.add(funding)
} }
fun getVirtualQueueItems(): List<Episode> {
var qString = "feedId == $id AND playState != ${Episode.PlayState.PLAYED.code}"
if (preferences?.prefStreamOverDownload != true) qString += " AND media.downloaded == true"
val eList_ = realm.query(Episode::class, qString).find().toMutableList()
if (sortOrder != null) getPermutor(sortOrder!!).reorder(eList_)
return eList_
}
companion object { companion object {
val TAG: String = Feed::class.simpleName ?: "Anonymous" val TAG: String = Feed::class.simpleName ?: "Anonymous"

View File

@ -1,7 +1,9 @@
package ac.mdiq.podcini.storage.model package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger
import androidx.compose.runtime.mutableStateOf
import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.ext.realmSetOf
import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.RealmSet
@ -46,6 +48,8 @@ class FeedPreferences : EmbeddedRealmObject {
} }
var volumeAdaption: Int = 0 var volumeAdaption: Int = 0
var prefStreamOverDownload: Boolean = false
var filterString: String = "" var filterString: String = ""
var sortOrderCode: Int = 0 // in EpisodeSortOrder var sortOrderCode: Int = 0 // in EpisodeSortOrder
@ -62,11 +66,31 @@ class FeedPreferences : EmbeddedRealmObject {
@Ignore @Ignore
var queue: PlayQueue? = null var queue: PlayQueue? = null
get() = if(queueId >= 0) realm.query(PlayQueue::class).query("id == $queueId").first().find() else null get() = when {
queueId >= 0 -> realm.query(PlayQueue::class).query("id == $queueId").first().find()
queueId == -1L -> curQueue
queueId == -2L -> null
else -> null
}
set(value) { set(value) {
field = value field = value
queueId = value?.id ?: -1L queueId = value?.id ?: -1L
} }
@Ignore
var queueText: String = "Default"
get() = when (queueId) {
0L -> "Default"
-1L -> "Active"
-2L -> "None"
else -> "Custom"
}
@Ignore
val queueTextExt: String
get() = when (queueId) {
-1L -> "Active"
-2L -> "None"
else -> queue?.name ?: "Default"
}
var queueId: Long = 0L var queueId: Long = 0L
@Ignore @Ignore

View File

@ -11,9 +11,7 @@ import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Episode.Companion.UNSPECIFIED
import ac.mdiq.podcini.storage.model.PlayQueue import ac.mdiq.podcini.storage.model.PlayQueue
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.utils.LocalDeleteModal import ac.mdiq.podcini.ui.utils.LocalDeleteModal
@ -47,7 +45,7 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
R.id.put_in_queue_batch -> PutToQueueDialog(activity, items).show() R.id.put_in_queue_batch -> PutToQueueDialog(activity, items).show()
R.id.remove_from_queue_batch -> removeFromQueueChecked(items) R.id.remove_from_queue_batch -> removeFromQueueChecked(items)
R.id.toggle_played_batch -> { R.id.toggle_played_batch -> {
setPlayState(UNSPECIFIED, false, *items.toTypedArray()) setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *items.toTypedArray())
// showMessage(R.plurals.marked_read_batch_label, items.size) // showMessage(R.plurals.marked_read_batch_label, items.size)
} }
// R.id.mark_read_batch -> { // R.id.mark_read_batch -> {

View File

@ -46,7 +46,8 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
episode.feed != null && episode.feed!!.isLocalFeed -> PlayLocalActionButton(episode) episode.feed != null && episode.feed!!.isLocalFeed -> PlayLocalActionButton(episode)
media.downloaded -> PlayActionButton(episode) media.downloaded -> PlayActionButton(episode)
isDownloadingMedia -> CancelDownloadActionButton(episode) isDownloadingMedia -> CancelDownloadActionButton(episode)
isStreamOverDownload || episode.feed == null || episode.feedId == null -> StreamActionButton(episode) isStreamOverDownload || episode.feed == null || episode.feedId == null || episode.feed?.preferences?.prefStreamOverDownload == true ->
StreamActionButton(episode)
else -> DownloadActionButton(episode) else -> DownloadActionButton(episode)
} }
} }

View File

@ -20,7 +20,7 @@ class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
} }
@UnstableApi override fun onClick(context: Context) { @UnstableApi override fun onClick(context: Context) {
if (!item.isPlayed()) setPlayState(Episode.PLAYED, true, item) if (!item.isPlayed()) setPlayState(Episode.PlayState.PLAYED.code, true, item)
} }
} }

View File

@ -1,14 +1,17 @@
package ac.mdiq.podcini.ui.actions.actionbutton package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.ui.actions.actionbutton.StreamActionButton.StreamingConfirmationDialog
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
@ -57,10 +60,11 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
fun notifyMissingEpisodeMediaFile(context: Context, media: EpisodeMedia) { fun notifyMissingEpisodeMediaFile(context: Context, media: EpisodeMedia) {
Logd(TAG, "notifyMissingEpisodeMediaFile called") Logd(TAG, "notifyMissingEpisodeMediaFile called")
Log.i(TAG, "The feedmanager was notified about a missing episode. It will update its database now.") Log.i(TAG, "The feedmanager was notified about a missing episode. It will update its database now.")
val episode = media.episodeOrFetch() val episode = realm.query(Episode::class).query("id == media.id").first().find()
// val episode = media.episodeOrFetch()
if (episode != null) { if (episode != null) {
val episode_ = upsertBlk(episode) { val episode_ = upsertBlk(episode) {
it.media = media // it.media = media
it.media?.downloaded = false it.media?.downloaded = false
it.media?.fileUrl = null it.media?.fileUrl = null
} }

View File

@ -141,7 +141,7 @@ object EpisodeMenuHandler {
} }
R.id.mark_read_item -> { R.id.mark_read_item -> {
// selectedItem.setPlayed(true) // selectedItem.setPlayed(true)
setPlayState(Episode.PLAYED, true, selectedItem) setPlayState(Episode.PlayState.PLAYED.code, true, selectedItem)
if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) { if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) {
val media: EpisodeMedia? = selectedItem.media val media: EpisodeMedia? = selectedItem.media
// not all items have media, Gpodder only cares about those that do // not all items have media, Gpodder only cares about those that do
@ -158,7 +158,7 @@ object EpisodeMenuHandler {
} }
R.id.mark_unread_item -> { R.id.mark_unread_item -> {
// selectedItem.setPlayed(false) // selectedItem.setPlayed(false)
setPlayState(Episode.UNPLAYED, false, selectedItem) setPlayState(Episode.PlayState.UNPLAYED.code, false, selectedItem)
if (needSynch() && selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) { if (needSynch() && selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) {
val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW) val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp() .currentTimestamp()
@ -176,7 +176,7 @@ object EpisodeMenuHandler {
writeNoMediaPlaying() writeNoMediaPlaying()
IntentUtils.sendLocalBroadcast(context, ACTION_SHUTDOWN_PLAYBACK_SERVICE) IntentUtils.sendLocalBroadcast(context, ACTION_SHUTDOWN_PLAYBACK_SERVICE)
} }
setPlayState(Episode.UNPLAYED, true, selectedItem) setPlayState(Episode.PlayState.UNPLAYED.code, true, selectedItem)
} }
R.id.visit_website_item -> { R.id.visit_website_item -> {
val url = selectedItem.getLinkWithFallback() val url = selectedItem.getLinkWithFallback()

View File

@ -61,7 +61,7 @@ class RemoveFromQueueSwipeAction : SwipeAction {
fun addToQueueAt(episode: Episode, index: Int) : Job { fun addToQueueAt(episode: Episode, index: Int) : Job {
return runOnIOScope { return runOnIOScope {
if (curQueue.episodeIds.contains(episode.id)) return@runOnIOScope if (curQueue.episodeIds.contains(episode.id)) return@runOnIOScope
if (episode.isNew) setPlayState(Episode.UNPLAYED, false, episode) if (episode.isNew) setPlayState(Episode.PlayState.UNPLAYED.code, false, episode)
curQueue = upsert(curQueue) { curQueue = upsert(curQueue) {
it.episodeIds.add(index, episode.id) it.episodeIds.add(index, episode.id)
it.update() it.update()

View File

@ -39,7 +39,7 @@ class TogglePlaybackStateSwipeAction : SwipeAction {
} }
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) { override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
val newState = if (item.playState == Episode.UNPLAYED) Episode.PLAYED else Episode.UNPLAYED val newState = if (item.playState == Episode.PlayState.UNPLAYED.code) Episode.PlayState.PLAYED.code else Episode.PlayState.UNPLAYED.code
Logd("TogglePlaybackStateSwipeAction", "performAction( ${item.id} )") Logd("TogglePlaybackStateSwipeAction", "performAction( ${item.id} )")
// we're marking it as unplayed since the user didn't actually play it // we're marking it as unplayed since the user didn't actually play it
@ -55,10 +55,10 @@ class TogglePlaybackStateSwipeAction : SwipeAction {
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) } if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) }
} }
val playStateStringRes: Int = when (newState) { val playStateStringRes: Int = when (newState) {
Episode.UNPLAYED -> if (item.playState == Episode.NEW) R.string.removed_inbox_label //was new Episode.PlayState.UNPLAYED.code -> if (item.playState == Episode.PlayState.NEW.code) R.string.removed_inbox_label //was new
else R.string.marked_as_unplayed_label //was played else R.string.marked_as_unplayed_label //was played
Episode.PLAYED -> R.string.marked_as_played_label Episode.PlayState.PLAYED.code -> R.string.marked_as_played_label
else -> if (item.playState == Episode.NEW) R.string.removed_inbox_label else -> if (item.playState == Episode.PlayState.NEW.code) R.string.removed_inbox_label
else R.string.marked_as_unplayed_label else R.string.marked_as_unplayed_label
} }
val duration: Int = Snackbar.LENGTH_LONG val duration: Int = Snackbar.LENGTH_LONG
@ -87,7 +87,7 @@ class TogglePlaybackStateSwipeAction : SwipeAction {
} }
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean { override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
return if (item.playState == Episode.NEW) filter.showPlayed || filter.showNew return if (item.playState == Episode.PlayState.NEW.code) filter.showPlayed || filter.showNew
else filter.showUnplayed || filter.showPlayed || filter.showNew else filter.showUnplayed || filter.showPlayed || filter.showNew
} }
} }

View File

@ -1,7 +1,6 @@
package ac.mdiq.podcini.ui.adapter package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
@ -92,6 +91,7 @@ open class EpisodesAdapter(mainActivity: MainActivity, var refreshFragPosCallbac
@UnstableApi @UnstableApi
override fun onBindViewHolder(holder: EpisodeViewHolder, pos: Int) { override fun onBindViewHolder(holder: EpisodeViewHolder, pos: Int) {
// Logd(TAG, "onBindViewHolder $pos ${holder.episode?.title}")
if (pos >= episodes.size || pos < 0) { if (pos >= episodes.size || pos < 0) {
beforeBindViewHolder(holder, pos) beforeBindViewHolder(holder, pos)
holder.bindDummy() holder.bindDummy()
@ -150,8 +150,10 @@ open class EpisodesAdapter(mainActivity: MainActivity, var refreshFragPosCallbac
@UnstableApi @UnstableApi
override fun onBindViewHolder(holder: EpisodeViewHolder, pos: Int, payloads: MutableList<Any>) { override fun onBindViewHolder(holder: EpisodeViewHolder, pos: Int, payloads: MutableList<Any>) {
// Logd(TAG, "onBindViewHolder payloads $pos ${holder.episode?.title}")
if (payloads.isEmpty()) onBindViewHolder(holder, pos) if (payloads.isEmpty()) onBindViewHolder(holder, pos)
else { else {
holder.refreshAdapterPosCallback = ::refreshPosCallback
val payload = payloads[0] val payload = payloads[0]
when { when {
payload is String && payload == "foo" -> onBindViewHolder(holder, pos) payload is String && payload == "foo" -> onBindViewHolder(holder, pos)

View File

@ -213,13 +213,14 @@ import java.util.*
if (nameEpisodeMap.isNotEmpty()) { if (nameEpisodeMap.isNotEmpty()) {
for (e in nameEpisodeMap.values) { for (e in nameEpisodeMap.values) {
upsertBlk(e) { upsertBlk(e) {
e.media?.setfileUrlOrNull(null) it.media?.setfileUrlOrNull(null)
} }
} }
} }
loadItems()
Logd(TAG, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}") Logd(TAG, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(requireContext().applicationContext, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}", Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}", Toast.LENGTH_LONG).show()
} }
} }
} }

View File

@ -573,7 +573,7 @@ import java.util.concurrent.Semaphore
val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) } val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) }
episodes.addAll(episodes_) episodes.addAll(episodes_)
} else episodes.addAll(feed_.episodes) } else episodes.addAll(feed_.episodes)
val sortOrder = fromCode(feed_.preferences?.sortOrderCode ?: 0) val sortOrder = feed_.sortOrder
if (sortOrder != null) getPermutor(sortOrder).reorder(episodes) if (sortOrder != null) getPermutor(sortOrder).reorder(episodes)
if (onInit) { if (onInit) {
var hasNonMediaItems = false var hasNonMediaItems = false

View File

@ -18,6 +18,8 @@ import ac.mdiq.podcini.ui.utils.TransitionEffect
import ac.mdiq.podcini.util.IntentUtils import ac.mdiq.podcini.util.IntentUtils
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.ShareUtils import ac.mdiq.podcini.util.ShareUtils
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.R.string import android.R.string
import android.app.Activity import android.app.Activity
import android.content.* import android.content.*
@ -46,10 +48,8 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
@ -64,8 +64,6 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var feed: Feed private lateinit var feed: Feed
// private lateinit var imgvCover: ImageView
// private lateinit var imgvBackground: ImageView
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) { private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) {
@ -131,6 +129,18 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
return binding.root return binding.root
} }
override fun onStart() {
Logd(TAG, "onStart() called")
super.onStart()
procFlowEvents()
}
override fun onStop() {
Logd(TAG, "onStop() called")
super.onStop()
cancelFlowEvents()
}
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
val horizontalSpacing = resources.getDimension(R.dimen.additional_horizontal_spacing).toInt() val horizontalSpacing = resources.getDimension(R.dimen.additional_horizontal_spacing).toInt()
@ -269,6 +279,23 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
} }
private var eventSink: Job? = null
private fun cancelFlowEvents() {
eventSink?.cancel()
eventSink = null
}
private fun procFlowEvents() {
if (eventSink == null) eventSink = lifecycleScope.launch {
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.FeedPrefsChangeEvent -> feed = event.feed
else -> {}
}
}
}
}
private class AddLocalFolder : ActivityResultContracts.OpenDocumentTree() { private class AddLocalFolder : ActivityResultContracts.OpenDocumentTree() {
override fun createIntent(context: Context, input: Uri?): Intent { override fun createIntent(context: Context, input: Uri?): Intent {
return super.createIntent(context, input).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) return super.createIntent(context, input).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

View File

@ -9,18 +9,16 @@ import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions
import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.Spinner
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.Spinner
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
@ -40,11 +38,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -70,6 +63,7 @@ class FeedSettingsFragment : Fragment() {
private val binding get() = _binding!! private val binding get() = _binding!!
private var feed: Feed? = null private var feed: Feed? = null
private var autoDeleteSummaryResId by mutableIntStateOf(R.string.global_default) private var autoDeleteSummaryResId by mutableIntStateOf(R.string.global_default)
private var curPrefQueue by mutableStateOf(feed?.preferences?.queueTextExt ?: "Default")
private var autoDeletePolicy = "global" private var autoDeletePolicy = "global"
private var queues: List<PlayQueue>? = null private var queues: List<PlayQueue>? = null
@ -102,6 +96,7 @@ class FeedSettingsFragment : Fragment() {
var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated ?: true) } var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated ?: true) }
Switch( Switch(
checked = checked, checked = checked,
modifier = Modifier.height(24.dp),
onCheckedChange = { onCheckedChange = {
checked = it checked = it
feed = upsertBlk(feed!!) { feed = upsertBlk(feed!!) {
@ -117,6 +112,35 @@ class FeedSettingsFragment : Fragment() {
) )
} }
Column { Column {
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_stream), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(
text = stringResource(R.string.pref_stream_over_download_title),
style = MaterialTheme.typography.h6,
color = textColor
)
Spacer(modifier = Modifier.weight(1f))
var checked by remember { mutableStateOf(feed?.preferences?.prefStreamOverDownload ?: false) }
Switch(
checked = checked,
modifier = Modifier.height(24.dp),
onCheckedChange = {
checked = it
feed = upsertBlk(feed!!) {
it.preferences?.prefStreamOverDownload = checked
}
}
)
}
Text(
text = stringResource(R.string.pref_stream_over_download_sum),
style = MaterialTheme.typography.body2,
color = textColor
)
}
Column {
curPrefQueue = feed?.preferences?.queueTextExt ?: "Default"
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor) Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
@ -125,27 +149,21 @@ class FeedSettingsFragment : Fragment() {
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,
color = textColor, color = textColor,
modifier = Modifier.clickable(onClick = { modifier = Modifier.clickable(onClick = {
// queues = realm.query(PlayQueue::class).find() val selectedOption = feed?.preferences?.queueText ?: "Default"
val selectedOption = when (feed?.preferences?.queueId) { val composeView = ComposeView(requireContext()).apply {
null, 0L -> "Default" setContent {
-1L -> "Active" val showDialog = remember { mutableStateOf(true) }
else -> "Custom" CustomTheme(requireContext()) {
} SetAssociatedQueue(showDialog.value, selectedOption = selectedOption, onDismissRequest = { showDialog.value = false })
val composeView = ComposeView(requireContext()).apply {
setContent {
val showDialog = remember { mutableStateOf(true) }
CustomTheme(requireContext()) {
SetAssociatedQueue(showDialog.value, selectedOption = selectedOption, onDismissRequest = { showDialog.value = false })
}
} }
} }
(view as? ViewGroup)?.addView(composeView) }
(view as? ViewGroup)?.addView(composeView)
}) })
) )
} }
Text( Text(
text = stringResource(R.string.pref_feed_associated_queue_sum), text = curPrefQueue + " : " + stringResource(R.string.pref_feed_associated_queue_sum),
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
color = textColor color = textColor
) )
@ -159,16 +177,15 @@ class FeedSettingsFragment : Fragment() {
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,
color = textColor, color = textColor,
modifier = Modifier.clickable(onClick = { modifier = Modifier.clickable(onClick = {
val composeView = ComposeView(requireContext()).apply { val composeView = ComposeView(requireContext()).apply {
setContent { setContent {
val showDialog = remember { mutableStateOf(true) } val showDialog = remember { mutableStateOf(true) }
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
AutoDeleteDialog(showDialog.value, onDismissRequest = { showDialog.value = false }) AutoDeleteDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
}
} }
} }
(view as? ViewGroup)?.addView(composeView) }
(view as? ViewGroup)?.addView(composeView)
}) })
) )
} }
@ -231,35 +248,29 @@ class FeedSettingsFragment : Fragment() {
) { ) {
Column { Column {
FeedAutoDeleteOptions.forEach { text -> FeedAutoDeleteOptions.forEach { text ->
Row( Row(Modifier
Modifier .fillMaxWidth()
.fillMaxWidth() .padding(horizontal = 16.dp),
.selectable(
selected = (text == selectedOption),
onClick = {
Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
val action_ = when (text) {
"global" -> AutoDeleteAction.GLOBAL
"always" -> AutoDeleteAction.ALWAYS
"never" -> AutoDeleteAction.NEVER
else -> AutoDeleteAction.GLOBAL
}
feed = upsertBlk(feed!!) {
it.preferences?.autoDeleteAction = action_
}
getAutoDeletePolicy()
onDismissRequest()
}
}
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( Checkbox(checked = (text == selectedOption),
selected = (text == selectedOption), onCheckedChange = { isChecked ->
onClick = { } Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
val action_ = when (text) {
"global" -> AutoDeleteAction.GLOBAL
"always" -> AutoDeleteAction.ALWAYS
"never" -> AutoDeleteAction.NEVER
else -> AutoDeleteAction.GLOBAL
}
feed = upsertBlk(feed!!) {
it.preferences?.autoDeleteAction = action_
}
getAutoDeletePolicy()
onDismissRequest()
}
}
) )
Text( Text(
text = text, text = text,
@ -281,37 +292,36 @@ class FeedSettingsFragment : Fragment() {
var selected by remember {mutableStateOf(selectedOption)} var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) { if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { Dialog(onDismissRequest = { onDismissRequest() }) {
Card( Card(modifier = Modifier
modifier = Modifier .wrapContentSize(align = Alignment.Center)
.wrapContentSize(align = Alignment.Center) .padding(16.dp),
.padding(16.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) { ) {
Column( Column(modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
queueSettingOptions.forEach { option -> queueSettingOptions.forEach { option ->
Row( Row(modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Checkbox( Checkbox(checked = option == selected,
checked = option == selected,
onCheckedChange = { isChecked -> onCheckedChange = { isChecked ->
selected = option selected = option
if (isChecked) Logd(TAG, "$option is checked") if (isChecked) Logd(TAG, "$option is checked")
when (selected) { when (selected) {
"Default" -> { "Default" -> {
feed = upsertBlk(feed!!) { feed = upsertBlk(feed!!) { it.preferences?.queueId = 0L }
it.preferences?.queueId = 0L curPrefQueue = selected
}
onDismissRequest() onDismissRequest()
} }
"Active" -> { "Active" -> {
feed = upsertBlk(feed!!) { feed = upsertBlk(feed!!) { it.preferences?.queueId = -1L }
it.preferences?.queueId = 1L curPrefQueue = selected
} onDismissRequest()
}
"None" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = -2L }
curPrefQueue = selected
onDismissRequest() onDismissRequest()
} }
"Custom" -> {} "Custom" -> {}
@ -326,9 +336,8 @@ class FeedSettingsFragment : Fragment() {
Spinner(items = queues!!.map { it.name }, selectedItem = feed?.preferences?.queue?.name ?: "Default") { name -> Spinner(items = queues!!.map { it.name }, selectedItem = feed?.preferences?.queue?.name ?: "Default") { name ->
Logd(TAG, "Queue selected: $name") Logd(TAG, "Queue selected: $name")
val q = queues?.firstOrNull { it.name == name } val q = queues?.firstOrNull { it.name == name }
feed = upsertBlk(feed!!) { feed = upsertBlk(feed!!) { it.preferences?.queue = q }
it.preferences?.queue = q if (q != null) curPrefQueue = q.name
}
onDismissRequest() onDismissRequest()
} }
} }
@ -753,7 +762,7 @@ class FeedSettingsFragment : Fragment() {
private val TAG: String = FeedSettingsFragment::class.simpleName ?: "Anonymous" private val TAG: String = FeedSettingsFragment::class.simpleName ?: "Anonymous"
private const val EXTRA_FEED_ID = "ac.mdiq.podcini.extra.feedId" private const val EXTRA_FEED_ID = "ac.mdiq.podcini.extra.feedId"
val queueSettingOptions = listOf("Default", "Active", "Custom") val queueSettingOptions = listOf("Default", "Active", "None", "Custom")
fun newInstance(feed: Feed): FeedSettingsFragment { fun newInstance(feed: Feed): FeedSettingsFragment {
val fragment = FeedSettingsFragment() val fragment = FeedSettingsFragment()

View File

@ -85,7 +85,7 @@ import java.util.*
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var recyclerView: EpisodesRecyclerView private lateinit var recyclerView: EpisodesRecyclerView
private lateinit var emptyView: EmptyViewHandler private lateinit var emptyViewHandler: EmptyViewHandler
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
private lateinit var swipeActions: SwipeActions private lateinit var swipeActions: SwipeActions
private lateinit var speedDialView: SpeedDialView private lateinit var speedDialView: SpeedDialView
@ -179,12 +179,12 @@ import java.util.*
adapter?.setOnSelectModeListener(this) adapter?.setOnSelectModeListener(this)
recyclerView.adapter = adapter recyclerView.adapter = adapter
emptyView = EmptyViewHandler(requireContext()) emptyViewHandler = EmptyViewHandler(requireContext())
emptyView.attachToRecyclerView(recyclerView) emptyViewHandler.attachToRecyclerView(recyclerView)
emptyView.setIcon(R.drawable.ic_playlist_play) emptyViewHandler.setIcon(R.drawable.ic_playlist_play)
emptyView.setTitle(R.string.no_items_header_label) emptyViewHandler.setTitle(R.string.no_items_header_label)
emptyView.setMessage(R.string.no_items_label) emptyViewHandler.setMessage(R.string.no_items_label)
emptyView.updateAdapter(adapter) emptyViewHandler.updateAdapter(adapter)
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root) val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
speedDialView = multiSelectDial.fabSD speedDialView = multiSelectDial.fabSD
@ -290,8 +290,12 @@ import java.util.*
} }
when (event.action) { when (event.action) {
FlowEvent.QueueEvent.Action.ADDED -> { FlowEvent.QueueEvent.Action.ADDED -> {
if (event.episodes.isNotEmpty() && !curQueue.isInQueue(event.episodes[0])) queueItems.add(event.position, event.episodes[0]) if (event.episodes.isNotEmpty() && !curQueue.isInQueue(event.episodes[0])) {
adapter?.notifyItemInserted(event.position) val pos = queueItems.size
queueItems.addAll(event.episodes)
adapter?.notifyItemRangeInserted(pos, queueItems.size)
adapter?.notifyItemRangeChanged(pos, event.episodes.size);
}
} }
FlowEvent.QueueEvent.Action.SET_QUEUE, FlowEvent.QueueEvent.Action.SORTED -> { FlowEvent.QueueEvent.Action.SET_QUEUE, FlowEvent.QueueEvent.Action.SORTED -> {
queueItems.clear() queueItems.clear()
@ -303,9 +307,12 @@ import java.util.*
for (e in event.episodes) { for (e in event.episodes) {
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id) val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
if (pos >= 0) { if (pos >= 0) {
Logd(TAG, "removing episode $pos ${queueItems[pos]} $e") Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
val holder = recyclerView.findViewHolderForLayoutPosition(pos) as? EpisodeViewHolder
holder?.unbind()
queueItems.removeAt(pos) queueItems.removeAt(pos)
adapter?.notifyItemRemoved(pos) adapter?.notifyItemRemoved(pos)
adapter?.notifyItemRangeChanged(pos, adapter!!.getItemCount()-pos);
} else { } else {
Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}") Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}")
continue continue
@ -637,18 +644,16 @@ import java.util.*
private fun loadCurQueue(restoreScrollPosition: Boolean) { private fun loadCurQueue(restoreScrollPosition: Boolean) {
if (!loadItemsRunning) { if (!loadItemsRunning) {
loadItemsRunning = true loadItemsRunning = true
adapter?.updateItems(mutableListOf()) // adapter?.updateItems(mutableListOf())
Logd(TAG, "loadCurQueue() called ${curQueue.name}") Logd(TAG, "loadCurQueue() called ${curQueue.name}")
while (curQueue.name.isEmpty()) runBlocking { delay(100) } while (curQueue.name.isEmpty()) runBlocking { delay(100) }
if (queueItems.isEmpty()) emptyView.hide() if (queueItems.isNotEmpty()) emptyViewHandler.hide()
queueItems.clear() queueItems.clear()
if (showBin) { if (showBin) {
queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) }) .find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
} else { } else {
curQueue.episodes.clear() curQueue.episodes.clear()
// curQueue.episodes.addAll(realm.query(Episode::class, "id IN $0", curQueue.episodeIds)
// .find().sortedBy { curQueue.episodeIds.indexOf(it.id) })
queueItems.addAll(curQueue.episodes) queueItems.addAll(curQueue.episodes)
} }
Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}") Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}")

View File

@ -388,7 +388,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
val dir = 1 - 2*feedOrderDir // get from 0, 1 to 1, -1 val dir = 1 - 2*feedOrderDir // get from 0, 1 to 1, -1
val comparator: Comparator<Feed> = when (feedOrder) { val comparator: Comparator<Feed> = when (feedOrder) {
FeedSortOrder.UNPLAYED_NEW_OLD.index -> { FeedSortOrder.UNPLAYED_NEW_OLD.index -> {
val queryString = "feedId == $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})" val queryString = "feedId == $0 AND (playState == ${Episode.PlayState.NEW.code} OR playState == ${Episode.PlayState.UNPLAYED.code})"
val counterMap: MutableMap<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feedList_) { for (f in feedList_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find() val c = realm.query(Episode::class).query(queryString, f.id).count().find()
@ -410,7 +410,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
} }
} }
FeedSortOrder.MOST_PLAYED.index -> { FeedSortOrder.MOST_PLAYED.index -> {
val queryString = "feedId == $0 AND playState == ${Episode.PLAYED}" val queryString = "feedId == $0 AND playState == ${Episode.PlayState.PLAYED.code}"
val counterMap: MutableMap<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feedList_) { for (f in feedList_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find() val c = realm.query(Episode::class).query(queryString, f.id).count().find()
@ -444,7 +444,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
} }
FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> { FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> {
val queryString = val queryString =
"feedId == $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) SORT(pubDate DESC)" "feedId == $0 AND (playState == ${Episode.PlayState.NEW.code} OR playState == ${Episode.PlayState.UNPLAYED.code}) SORT(pubDate DESC)"
val counterMap: MutableMap<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feedList_) { for (f in feedList_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L
@ -466,7 +466,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
} }
FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> { FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> {
val queryString = val queryString =
"feedId == $0 AND (playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true" "feedId == $0 AND (playState == ${Episode.PlayState.NEW.code} OR playState == ${Episode.PlayState.UNPLAYED.code}) AND media.downloaded == true"
val counterMap: MutableMap<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feedList_) { for (f in feedList_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find() val c = realm.query(Episode::class).query(queryString, f.id).count().find()
@ -477,7 +477,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
} }
// doing FEED_ORDER_NEW // doing FEED_ORDER_NEW
else -> { else -> {
val queryString = "feedId == $0 AND playState == ${Episode.NEW}" val queryString = "feedId == $0 AND playState == ${Episode.PlayState.NEW.code}"
val counterMap: MutableMap<Long, Long> = mutableMapOf() val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feedList_) { for (f in feedList_) {
val c = realm.query(Episode::class).query(queryString, f.id).count().find() val c = realm.query(Episode::class).query(queryString, f.id).count().find()
@ -738,37 +738,33 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
var selected by remember {mutableStateOf("")} var selected by remember {mutableStateOf("")}
if (showDialog) { if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { Dialog(onDismissRequest = { onDismissRequest() }) {
Card( Card(modifier = Modifier
modifier = Modifier .wrapContentSize(align = Alignment.Center)
.wrapContentSize(align = Alignment.Center) .padding(16.dp),
.padding(16.dp),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
) { ) {
Column( Column(modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
queueSettingOptions.forEach { option -> queueSettingOptions.forEach { option ->
Row( Row(modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Checkbox( Checkbox(checked = option == selected,
checked = option == selected,
onCheckedChange = { isChecked -> onCheckedChange = { isChecked ->
selected = option selected = option
if (isChecked) Logd(TAG, "$option is checked") if (isChecked) Logd(TAG, "$option is checked")
when (selected) { when (selected) {
"Default" -> { "Default" -> {
saveFeedPreferences { it: FeedPreferences -> saveFeedPreferences { it: FeedPreferences -> it.queueId = 0L }
it.queueId = 0L
}
onDismissRequest() onDismissRequest()
} }
"Active" -> { "Active" -> {
saveFeedPreferences { it: FeedPreferences -> saveFeedPreferences { it: FeedPreferences -> it.queueId = -1L }
it.queueId = -1L onDismissRequest()
} }
"None" -> {
saveFeedPreferences { it: FeedPreferences -> it.queueId = -2L }
onDismissRequest() onDismissRequest()
} }
"Custom" -> {} "Custom" -> {}
@ -784,9 +780,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
Logd(TAG, "Queue selected: $name") Logd(TAG, "Queue selected: $name")
val q = queues.firstOrNull { it.name == name } val q = queues.firstOrNull { it.name == name }
if (q != null) { if (q != null) {
saveFeedPreferences { it: FeedPreferences -> saveFeedPreferences { it: FeedPreferences -> it.queueId = q.id }
it.queueId = q.id
}
onDismissRequest() onDismissRequest()
} }
} }

View File

@ -447,7 +447,7 @@ class StatisticsFragment : Fragment() {
else { else {
// progress import does not include playedDuration // progress import does not include playedDuration
if (includeMarkedAsPlayed) { if (includeMarkedAsPlayed) {
if (m.playbackCompletionTime > 0 || m.episodeOrFetch()?.playState == Episode.PLAYED) if (m.playbackCompletionTime > 0 || m.episodeOrFetch()?.playState == Episode.PlayState.PLAYED.code)
dur += m.duration dur += m.duration
else if (m.position > 0) dur += m.position else if (m.position > 0) dur += m.position
} else dur += m.position } else dur += m.position

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.ui.utils package ac.mdiq.podcini.ui.utils
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodeMedia
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -13,7 +13,7 @@ object LocalDeleteModal {
val localItems: MutableList<Episode> = mutableListOf() val localItems: MutableList<Episode> = mutableListOf()
for (item in items) { for (item in items) {
if (item.feed?.isLocalFeed == true) localItems.add(item) if (item.feed?.isLocalFeed == true) localItems.add(item)
else deleteMediaOfEpisode(context, item) else deleteEpisodeMedia(context, item)
} }
if (localItems.isNotEmpty()) { if (localItems.isNotEmpty()) {
@ -22,7 +22,7 @@ object LocalDeleteModal {
.setMessage(R.string.delete_local_feed_warning_body) .setMessage(R.string.delete_local_feed_warning_body)
.setPositiveButton(R.string.delete_label) { dialog: DialogInterface?, which: Int -> .setPositiveButton(R.string.delete_label) { dialog: DialogInterface?, which: Int ->
for (item in localItems) { for (item in localItems) {
deleteMediaOfEpisode(context, item) deleteEpisodeMedia(context, item)
} }
} }
.setNegativeButton(R.string.cancel_label, null) .setNegativeButton(R.string.cancel_label, null)

View File

@ -7,9 +7,8 @@ import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Episode.Companion.BUILDING import ac.mdiq.podcini.storage.model.Episode.PlayState
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Feed.Companion.PREFIX_GENERATIVE_COVER import ac.mdiq.podcini.storage.model.Feed.Companion.PREFIX_GENERATIVE_COVER
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.MediaType
@ -99,7 +98,6 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro
} }
fun bind(item: Episode) { fun bind(item: Episode) {
// Logd(TAG, "in bind: ${item.title} ${item.isFavorite} ${item.isPlayed()}")
if (episodeMonitor == null) { if (episodeMonitor == null) {
val item_ = realm.query(Episode::class).query("id == ${item.id}").first() val item_ = realm.query(Episode::class).query("id == ${item.id}").first()
episodeMonitor = CoroutineScope(Dispatchers.Default).launch { episodeMonitor = CoroutineScope(Dispatchers.Default).launch {
@ -116,6 +114,7 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro
else -> {} else -> {}
} }
} }
// return
} }
} }
if (mediaMonitor == null) { if (mediaMonitor == null) {
@ -174,7 +173,7 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro
when { when {
item.media != null -> bind(item.media!!) item.media != null -> bind(item.media!!)
// for generating TTS files for episode without media // for generating TTS files for episode without media
item.playState == BUILDING -> { item.playState == PlayState.BUILDING.code -> {
secondaryActionProgress.setPercentage(actionButton!!.processing, item) secondaryActionProgress.setPercentage(actionButton!!.processing, item)
secondaryActionProgress.setIndeterminate(false) secondaryActionProgress.setIndeterminate(false)
} }

View File

@ -512,7 +512,7 @@
<string name="pref_feed_skip">Auto skip</string> <string name="pref_feed_skip">Auto skip</string>
<string name="pref_feed_skip_sum">Skip introductions and ending credits.</string> <string name="pref_feed_skip_sum">Skip introductions and ending credits.</string>
<string name="pref_feed_associated_queue">Associated queue</string> <string name="pref_feed_associated_queue">Associated queue</string>
<string name="pref_feed_associated_queue_sum">Set the queue to which epiosdes in the feed will added by default.</string> <string name="pref_feed_associated_queue_sum">The queue to which epiosdes in the feed will added by default</string>
<string name="pref_feed_skip_ending">Skip last</string> <string name="pref_feed_skip_ending">Skip last</string>
<string name="pref_feed_skip_intro">Skip first</string> <string name="pref_feed_skip_intro">Skip first</string>
<string name="pref_feed_skip_ending_toast">Skipped last %d seconds</string> <string name="pref_feed_skip_ending_toast">Skipped last %d seconds</string>

View File

@ -8,7 +8,7 @@ internal object FeedItemMother {
@JvmStatic @JvmStatic
fun anyFeedItemWithImage(): Episode { fun anyFeedItemWithImage(): Episode {
val item = Episode(0, "Item", "Item", "url", Date(), Episode.PLAYED, FeedMother.anyFeed()) val item = Episode(0, "Item", "Item", "url", Date(), Episode.PlayState.PLAYED.code, FeedMother.anyFeed())
item.imageUrl = (IMAGE_URL) item.imageUrl = (IMAGE_URL)
return item return item
} }

View File

@ -87,7 +87,7 @@ open class DbCleanupTests {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numItems, feed, items, files, Episode.PLAYED, false, false) populateItems(numItems, feed, items, files, Episode.PlayState.PLAYED.code, false, false)
performAutoCleanup(context) performAutoCleanup(context)
for (i in files.indices) { for (i in files.indices) {
@ -104,7 +104,7 @@ open class DbCleanupTests {
for (i in 0 until numItems) { for (i in 0 until numItems) {
val itemDate = Date((numItems - i).toLong()) val itemDate = Date((numItems - i).toLong())
var playbackCompletionDate: Date? = null var playbackCompletionDate: Date? = null
if (itemState == Episode.PLAYED) { if (itemState == Episode.PlayState.PLAYED.code) {
playbackCompletionDate = itemDate playbackCompletionDate = itemDate
} }
val item = Episode(0, "title", "id$i", "link", itemDate, itemState, feed) val item = Episode(0, "title", "id$i", "link", itemDate, itemState, feed)
@ -140,7 +140,7 @@ open class DbCleanupTests {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numItems, feed, items, files, Episode.UNPLAYED, false, false) populateItems(numItems, feed, items, files, Episode.PlayState.UNPLAYED.code, false, false)
performAutoCleanup(context) performAutoCleanup(context)
for (file in files) { for (file in files) {
@ -157,7 +157,7 @@ open class DbCleanupTests {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numItems, feed, items, files, Episode.PLAYED, true, false) populateItems(numItems, feed, items, files, Episode.PlayState.PLAYED.code, true, false)
performAutoCleanup(context) performAutoCleanup(context)
for (file in files) { for (file in files) {
@ -198,7 +198,7 @@ open class DbCleanupTests {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numItems, feed, items, files, Episode.PLAYED, false, true) populateItems(numItems, feed, items, files, Episode.PlayState.PLAYED.code, false, true)
performAutoCleanup(context) performAutoCleanup(context)
for (file in files) { for (file in files) {

View File

@ -81,7 +81,7 @@ class DbNullCleanupAlgorithmTest {
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
for (i in 0 until numItems) { for (i in 0 until numItems) {
val item = Episode(0, "title", "id$i", "link", Date(), Episode.PLAYED, feed) val item = Episode(0, "title", "id$i", "link", Date(), Episode.PlayState.PLAYED.code, feed)
val f = File(destFolder, "file $i") val f = File(destFolder, "file $i")
Assert.assertTrue(f.createNewFile()) Assert.assertTrue(f.createNewFile())

View File

@ -33,7 +33,7 @@ class DbPlayQueueCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numItems, feed, items, files, Episode.UNPLAYED, false, false) populateItems(numItems, feed, items, files, Episode.PlayState.UNPLAYED.code, false, false)
performAutoCleanup(context) performAutoCleanup(context)
for (i in files.indices) { for (i in files.indices) {

View File

@ -61,7 +61,7 @@ class DbTasksTest {
feed.episodes.clear() feed.episodes.clear()
for (i in 0 until numItems) { for (i in 0 until numItems) {
feed.episodes.add(Episode(0, "item $i", "id $i", "link $i", feed.episodes.add(Episode(0, "item $i", "id $i", "link $i",
Date(), Episode.UNPLAYED, feed)) Date(), Episode.PlayState.UNPLAYED.code, feed))
} }
val newFeed = updateFeed(context, feed, false) val newFeed = updateFeed(context, feed, false)
@ -97,7 +97,7 @@ class DbTasksTest {
feed.episodes.clear() feed.episodes.clear()
for (i in 0 until numItemsOld) { for (i in 0 until numItemsOld) {
feed.episodes.add(Episode(0, "item $i", "id $i", "link $i", feed.episodes.add(Episode(0, "item $i", "id $i", "link $i",
Date(i.toLong()), Episode.PLAYED, feed)) Date(i.toLong()), Episode.PlayState.PLAYED.code, feed))
} }
// val adapter = getInstance() // val adapter = getInstance()
// adapter.open() // adapter.open()
@ -117,7 +117,7 @@ class DbTasksTest {
for (i in numItemsOld until numItemsNew + numItemsOld) { for (i in numItemsOld until numItemsNew + numItemsOld) {
feed.episodes.add(0, Episode(0, "item $i", "id $i", "link $i", feed.episodes.add(0, Episode(0, "item $i", "id $i", "link $i",
Date(i.toLong()), Episode.UNPLAYED, feed)) Date(i.toLong()), Episode.PlayState.UNPLAYED.code, feed))
} }
val newFeed = updateFeed(context, feed, false) val newFeed = updateFeed(context, feed, false)
@ -134,7 +134,7 @@ class DbTasksTest {
@Test @Test
fun testUpdateFeedMediaUrlResetState() { fun testUpdateFeedMediaUrlResetState() {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
val item = Episode(0, "item", "id", "link", Date(), Episode.PLAYED, feed) val item = Episode(0, "item", "id", "link", Date(), Episode.PlayState.PLAYED.code, feed)
feed.episodes.add(item) feed.episodes.add(item)
// val adapter = getInstance() // val adapter = getInstance()
@ -166,7 +166,7 @@ class DbTasksTest {
feed.episodes.clear() feed.episodes.clear()
for (i in 0..9) { for (i in 0..9) {
feed.episodes.add( feed.episodes.add(
Episode(0, "item $i", "id $i", "link $i", Date(i.toLong()), Episode.PLAYED, feed)) Episode(0, "item $i", "id $i", "link $i", Date(i.toLong()), Episode.PlayState.PLAYED.code, feed))
} }
// val adapter = getInstance() // val adapter = getInstance()
// adapter.open() // adapter.open()
@ -188,7 +188,7 @@ class DbTasksTest {
feed.episodes.clear() feed.episodes.clear()
for (i in 0..9) { for (i in 0..9) {
val item = val item =
Episode(0, "item $i", "id $i", "link $i", Date(i.toLong()), Episode.PLAYED, feed) Episode(0, "item $i", "id $i", "link $i", Date(i.toLong()), Episode.PlayState.PLAYED.code, feed)
val media = EpisodeMedia(item, "download url $i", 123, "media/mp3") val media = EpisodeMedia(item, "download url $i", 123, "media/mp3")
item.setMedia(media) item.setMedia(media)
feed.episodes.add(item) feed.episodes.add(item)
@ -244,7 +244,7 @@ class DbTasksTest {
val items: MutableList<Episode> = ArrayList(numFeedItems) val items: MutableList<Episode> = ArrayList(numFeedItems)
for (i in 1..numFeedItems) { for (i in 1..numFeedItems) {
val item = Episode(0, "item $i of $title", "id$title$i", "link", val item = Episode(0, "item $i of $title", "id$title$i", "link",
Date(), Episode.UNPLAYED, feed) Date(), Episode.PlayState.UNPLAYED.code, feed)
items.add(item) items.add(item)
} }
feed.episodes.addAll(items) feed.episodes.addAll(items)

View File

@ -6,7 +6,7 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Episodes.addToHistory import ac.mdiq.podcini.storage.database.Episodes.addToHistory
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.getEpisode import ac.mdiq.podcini.storage.database.Episodes.getEpisode
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
@ -97,7 +97,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val item = Episode(0, "Item", "Item", "url", Date(), Episode.PLAYED, feed) val item = Episode(0, "Item", "Item", "url", Date(), Episode.PlayState.PLAYED.code, feed)
items.add(item) items.add(item)
val media = EpisodeMedia(0, item, duration, 1, 1, "mime_type", val media = EpisodeMedia(0, item, duration, 1, 1, "mime_type",
"dummy path", "download_url", true, null, 0, 0) "dummy path", "download_url", true, null, 0, 0)
@ -138,7 +138,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val item = Episode(0, "Item", "Item", "url", Date(), Episode.PLAYED, feed) val item = Episode(0, "Item", "Item", "url", Date(), Episode.PlayState.PLAYED.code, feed)
var media: EpisodeMedia? = EpisodeMedia(0, item, 1, 1, 1, "mime_type", var media: EpisodeMedia? = EpisodeMedia(0, item, 1, 1, 1, "mime_type",
dest.absolutePath, "download_url", true, null, 0, 0) dest.absolutePath, "download_url", true, null, 0, 0)
@ -154,7 +154,7 @@ class DbWriterTest {
Assert.assertTrue(item.id != 0L) Assert.assertTrue(item.id != 0L)
runBlocking { runBlocking {
val job = deleteMediaOfEpisode(context, item) val job = deleteEpisodeMedia(context, item)
withTimeout(TIMEOUT*1000) { job.join() } withTimeout(TIMEOUT*1000) { job.join() }
} }
media = getEpisodeMedia(media.id) media = getEpisodeMedia(media.id)
@ -176,7 +176,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val item = Episode(0, "Item", "Item", "url", Date(), Episode.UNPLAYED, feed) val item = Episode(0, "Item", "Item", "url", Date(), Episode.PlayState.UNPLAYED.code, feed)
var media: EpisodeMedia? = EpisodeMedia(0, item, 1, 1, 1, "mime_type", var media: EpisodeMedia? = EpisodeMedia(0, item, 1, 1, 1, "mime_type",
dest.absolutePath, "download_url", true, null, 0, 0) dest.absolutePath, "download_url", true, null, 0, 0)
@ -196,7 +196,7 @@ class DbWriterTest {
queue = curQueue.episodes queue = curQueue.episodes
Assert.assertTrue(queue.size != 0) Assert.assertTrue(queue.size != 0)
deleteMediaOfEpisode(context, item) deleteEpisodeMedia(context, item)
Awaitility.await().timeout(2, TimeUnit.SECONDS).until { !dest.exists() } Awaitility.await().timeout(2, TimeUnit.SECONDS).until { !dest.exists() }
media = getEpisodeMedia(media.id) media = getEpisodeMedia(media.id)
Assert.assertNotNull(media) Assert.assertNotNull(media)
@ -218,7 +218,7 @@ class DbWriterTest {
val itemFiles: MutableList<File> = ArrayList() val itemFiles: MutableList<File> = ArrayList()
// create items with downloaded media files // create items with downloaded media files
for (i in 0..9) { for (i in 0..9) {
val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PLAYED, feed) val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PlayState.PLAYED.code, feed)
feed.episodes.add(item) feed.episodes.add(item)
val enc = File(destFolder, "file $i") val enc = File(destFolder, "file $i")
@ -308,7 +308,7 @@ class DbWriterTest {
// create items // create items
for (i in 0..9) { for (i in 0..9) {
val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PLAYED, feed) val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PlayState.PLAYED.code, feed)
feed.episodes.add(item) feed.episodes.add(item)
} }
@ -352,7 +352,7 @@ class DbWriterTest {
// create items with downloaded media files // create items with downloaded media files
for (i in 0..9) { for (i in 0..9) {
val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PLAYED, feed) val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PlayState.PLAYED.code, feed)
feed.episodes.add(item) feed.episodes.add(item)
val enc = File(destFolder, "file $i") val enc = File(destFolder, "file $i")
val media = EpisodeMedia(0, item, 1, 1, 1, "mime_type", val media = EpisodeMedia(0, item, 1, 1, 1, "mime_type",
@ -415,7 +415,7 @@ class DbWriterTest {
// create items with downloaded media files // create items with downloaded media files
for (i in 0..9) { for (i in 0..9) {
val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PLAYED, feed) val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PlayState.PLAYED.code, feed)
feed.episodes.add(item) feed.episodes.add(item)
val enc = File(destFolder, "file $i") val enc = File(destFolder, "file $i")
val media = EpisodeMedia(0, item, 1, 1, 1, "mime_type", val media = EpisodeMedia(0, item, 1, 1, 1, "mime_type",
@ -463,7 +463,7 @@ class DbWriterTest {
// create items // create items
for (i in 0..9) { for (i in 0..9) {
val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PLAYED, feed) val item = Episode(0, "Item $i", "Item$i", "url", Date(), Episode.PlayState.PLAYED.code, feed)
item.setMedia(EpisodeMedia(item, "", 0, "")) item.setMedia(EpisodeMedia(item, "", 0, ""))
feed.episodes.add(item) feed.episodes.add(item)
} }
@ -494,7 +494,7 @@ class DbWriterTest {
private fun playbackHistorySetup(playbackCompletionDate: Date?): EpisodeMedia { private fun playbackHistorySetup(playbackCompletionDate: Date?): EpisodeMedia {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
feed.episodes.clear() feed.episodes.clear()
val item = Episode(0, "title", "id", "link", Date(), Episode.PLAYED, feed) val item = Episode(0, "title", "id", "link", Date(), Episode.PlayState.PLAYED.code, feed)
val media = EpisodeMedia(0, item, 10, 0, 1, "mime", null, val media = EpisodeMedia(0, item, 10, 0, 1, "mime", null,
"url", false, playbackCompletionDate, 0, 0) "url", false, playbackCompletionDate, 0, 0)
feed.episodes.add(item) feed.episodes.add(item)
@ -547,7 +547,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
feed.episodes.clear() feed.episodes.clear()
for (i in 0 until numItems) { for (i in 0 until numItems) {
val item = Episode(0, "title $i", "id $i", "link $i", Date(), Episode.PLAYED, feed) val item = Episode(0, "title $i", "id $i", "link $i", Date(), Episode.PlayState.PLAYED.code, feed)
item.setMedia(EpisodeMedia(item, "", 0, "")) item.setMedia(EpisodeMedia(item, "", 0, ""))
feed.episodes.add(item) feed.episodes.add(item)
} }
@ -576,7 +576,7 @@ class DbWriterTest {
fun testAddQueueItemSingleItem() { fun testAddQueueItemSingleItem() {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
feed.episodes.clear() feed.episodes.clear()
val item = Episode(0, "title", "id", "link", Date(), Episode.PLAYED, feed) val item = Episode(0, "title", "id", "link", Date(), Episode.PlayState.PLAYED.code, feed)
item.setMedia(EpisodeMedia(item, "", 0, "")) item.setMedia(EpisodeMedia(item, "", 0, ""))
feed.episodes.add(item) feed.episodes.add(item)
@ -605,7 +605,7 @@ class DbWriterTest {
fun testAddQueueItemSingleItemAlreadyInQueue() { fun testAddQueueItemSingleItemAlreadyInQueue() {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
feed.episodes.clear() feed.episodes.clear()
val item = Episode(0, "title", "id", "link", Date(), Episode.PLAYED, feed) val item = Episode(0, "title", "id", "link", Date(), Episode.PlayState.PLAYED.code, feed)
item.setMedia(EpisodeMedia(item, "", 0, "")) item.setMedia(EpisodeMedia(item, "", 0, ""))
feed.episodes.add(item) feed.episodes.add(item)
@ -766,7 +766,7 @@ class DbWriterTest {
feed.episodes.clear() feed.episodes.clear()
for (i in 0 until numItems) { for (i in 0 until numItems) {
val item = Episode(0, "title $i", "id $i", "link $i", val item = Episode(0, "title $i", "id $i", "link $i",
Date(), Episode.PLAYED, feed) Date(), Episode.PlayState.PLAYED.code, feed)
item.setMedia(EpisodeMedia(item, "", 0, "")) item.setMedia(EpisodeMedia(item, "", 0, ""))
feed.episodes.add(item) feed.episodes.add(item)
} }
@ -817,7 +817,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
feed.episodes.clear() feed.episodes.clear()
for (i in 0 until numItems) { for (i in 0 until numItems) {
val item = Episode(0, "title $i", "id $i", "link $i", Date(), Episode.NEW, feed) val item = Episode(0, "title $i", "id $i", "link $i", Date(), Episode.PlayState.NEW.code, feed)
item.setMedia(EpisodeMedia(item, "", 0, "")) item.setMedia(EpisodeMedia(item, "", 0, ""))
feed.episodes.add(item) feed.episodes.add(item)
} }
@ -848,7 +848,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
feed.episodes.clear() feed.episodes.clear()
for (i in 0 until numItems) { for (i in 0 until numItems) {
val item = Episode(0, "title $i", "id $i", "link $i", Date(), Episode.PLAYED, feed) val item = Episode(0, "title $i", "id $i", "link $i", Date(), Episode.PlayState.PLAYED.code, feed)
item.setMedia(EpisodeMedia(item, "", 0, "")) item.setMedia(EpisodeMedia(item, "", 0, ""))
feed.episodes.add(item) feed.episodes.add(item)
} }

View File

@ -57,7 +57,7 @@ class EpisodeDuplicateGuesserTest {
private fun item(guid: String, title: String, downloadUrl: String, private fun item(guid: String, title: String, downloadUrl: String,
date: Long, duration: Long, mime: String date: Long, duration: Long, mime: String
): Episode { ): Episode {
val item = Episode(0, title, guid, "link", Date(date), Episode.PLAYED, null) val item = Episode(0, title, guid, "link", Date(date), Episode.PlayState.PLAYED.code, null)
val media = EpisodeMedia(item, downloadUrl, duration, mime) val media = EpisodeMedia(item, downloadUrl, duration, mime)
item.setMedia(media) item.setMedia(media)
return item return item

View File

@ -29,7 +29,7 @@ class ExceptFavoriteCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numberOfItems, feed, items, files, Episode.UNPLAYED, false, false) populateItems(numberOfItems, feed, items, files, Episode.PlayState.UNPLAYED.code, false, false)
performAutoCleanup(context) performAutoCleanup(context)
for (i in files.indices) { for (i in files.indices) {
@ -48,7 +48,7 @@ class ExceptFavoriteCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numberOfItems, feed, items, files, Episode.UNPLAYED, true, false) populateItems(numberOfItems, feed, items, files, Episode.PlayState.UNPLAYED.code, true, false)
performAutoCleanup(context) performAutoCleanup(context)
for (i in files.indices) { for (i in files.indices) {
@ -67,7 +67,7 @@ class ExceptFavoriteCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList() val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items) feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList() val files: MutableList<File> = ArrayList()
populateItems(numberOfItems, feed, items, files, Episode.UNPLAYED, false, true) populateItems(numberOfItems, feed, items, files, Episode.PlayState.UNPLAYED.code, false, true)
performAutoCleanup(context) performAutoCleanup(context)
for (i in files.indices) { for (i in files.indices) {

View File

@ -57,7 +57,7 @@ object ItemEnqueuePositionCalculatorTest {
fun createFeedItem(id: Long): Episode { fun createFeedItem(id: Long): Episode {
val item = Episode(id, "Item$id", "ItemId$id", "url", val item = Episode(id, "Item$id", "ItemId$id", "url",
Date(), Episode.PLAYED, anyFeed()) Date(), Episode.PlayState.PLAYED.code, anyFeed())
val media = EpisodeMedia(item, "http://download.url.net/$id", 1234567, "audio/mpeg") val media = EpisodeMedia(item, "http://download.url.net/$id", 1234567, "audio/mpeg")
media.id = item.id media.id = item.id
item.setMedia(media) item.setMedia(media)

View File

@ -1,3 +1,20 @@
# 6.3.4
* fixed mis-behavior of setting associated queue to Active in FeedSettings
* items on dialog for "Auto delete episodes" are changed to checkbox
* playState Int variables have been put into enum PlayState
* corrected an error in incomplete reconsile
* fixed the nasty mis-behavior in Queues view when removing episodes
* updated feed in FeedInfo view when feed preferences change
* enhanced feed setting UI, added display of current queue preference
* added "prefer streaming over download" in feed setting. ruling along the global setting, streaming is preferred when either one is set to true
* added None in associated queue setting of any feed
* if set, episodes in the feed are not automatically added to any queue, but are used as a natural queue for getting the next episode to play
* the next episode is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
* else if "prefer streaming" is set, it's the next unplayed episode in the feed episodes list based on the current sort order
* else it's the next downloaded unplayed episode
# 6.3.3 # 6.3.3
* fixed crash when setting as Played/Unplayed in EpisodeInfo view * fixed crash when setting as Played/Unplayed in EpisodeInfo view

View File

@ -0,0 +1,17 @@
Version 6.3.4 brings several changes:
* fixed mis-behavior of setting associated queue to Active in FeedSettings
* items on dialog for "Auto delete episodes" are changed to checkbox
* playState Int variables have been put into enum PlayState
* corrected an error in incomplete reconsile
* fixed the nasty mis-behavior in Queues view when removing episodes
* updated feed in FeedInfo view when feed preferences change
* enhanced feed setting UI, added display of current queue preference
* added "prefer streaming over download" in feed setting. ruling along the global setting, streaming is preferred when either one is set to true
* added None in associated queue setting of any feed
* if set, episodes in the feed are not automatically added to any queue, but are used as a natural queue for getting the next episode to play
* the next episode is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
* else if "prefer streaming" is set, it's the next unplayed episode in the feed episodes list based on the current sort order
* else it's the next downloaded unplayed episode