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),
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,
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
8. Accepts podcast as well as plain RSS and YouTube feeds,
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
* 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
* "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
* 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 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 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

View File

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

View File

@ -105,7 +105,7 @@ class MediaPlayerBaseTest {
VolumeAdaptionSetting.OFF, null, null)
f.preferences = prefs
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)
val media = EpisodeMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0, 0)
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")
f.episodes.clear()
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()
// adapter.open()

View File

@ -113,7 +113,7 @@ class UITestUtils(private val context: Context) {
val items: MutableList<Episode> = ArrayList()
for (j in 0 until NUM_ITEMS_PER_FEED) {
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)
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.LogsAndStats
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.model.DownloadResult
import ac.mdiq.podcini.storage.model.Episode
@ -78,7 +77,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Logd(TAG, "starting cancel")
// This needs to be done here, not in the worker. Reason: The worker might or might not be running.
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 future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)
@ -124,6 +123,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
.addTag(WORK_TAG)
.addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl)
if (enqueueDownloadedEpisodes()) {
if (item.feed?.preferences?.queue != null)
runBlocking { Queues.addToQueueSync(false, item, item.feed?.preferences?.queue) }
workRequest.addTag(WORK_DATA_WAS_QUEUED)
}

View File

@ -132,7 +132,7 @@ object LocalFeedUpdater {
}
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()
val size = file.length

View File

@ -253,8 +253,11 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
curMedia = playable
if (curMedia is 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
prevMedia = curMedia
this.isStreaming = stream
mediaType = curMedia!!.getMediaType()

View File

@ -311,7 +311,7 @@ class PlaybackService : MediaSessionService() {
if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode())) {
Logd(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped")
// 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 shouldAutoDelete = (action == AutoDeleteAction.ALWAYS ||
(action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!)))
@ -355,11 +355,6 @@ class PlaybackService : MediaSessionService() {
override fun getNextInQueue(currentMedia: Playable?): Playable? {
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) {
Logd(TAG, "getNextInQueue(), but playable not an instance of EpisodeMedia, so not proceeding")
writeNoMediaPlaying()
@ -371,20 +366,29 @@ class PlaybackService : MediaSessionService() {
writeNoMediaPlaying()
return null
}
// val nextItem = getNextInQueue(item)
if (curQueue.episodes.isEmpty()) {
if (curIndexInQueue < 0 && item.feed?.preferences?.queue != null) {
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")
writeNoMediaPlaying()
return null
}
Logd(TAG, "getNextInQueue eList: ${eList.size}")
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 (curIndexInQueue < curQueue.episodes.size) j = curIndexInQueue
else j = curQueue.episodes.size-1
} else if (i < curQueue.episodes.size-1) j = i+1
if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) j = curIndexInQueue
else j = eList.size-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) {
Logd(TAG, "getNextInQueue nextItem: $nextItem media is null")
writeNoMediaPlaying()
@ -397,7 +401,7 @@ class PlaybackService : MediaSessionService() {
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}")
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent)
writeNoMediaPlaying()
@ -405,7 +409,7 @@ class PlaybackService : MediaSessionService() {
}
EventFlow.postEvent(FlowEvent.PlayEvent(item, FlowEvent.PlayEvent.Action.END))
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? {
@ -419,15 +423,13 @@ class PlaybackService : MediaSessionService() {
if (stopPlaying) taskManager.cancelPositionSaver()
if (mediaType == null) sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0)
else {
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
else sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
when {
isCasting -> EXTRA_CODE_CAST
mediaType == MediaType.VIDEO -> EXTRA_CODE_VIDEO
else -> EXTRA_CODE_AUDIO
})
}
}
override fun ensureMediaInfoLoaded(media: Playable) {
// if (media is EpisodeMedia && media.item == null) media.item = DBReader.getFeedItem(media.itemId)
@ -962,7 +964,7 @@ class PlaybackService : MediaSessionService() {
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
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) {
mPlayer?.endPlayback(hasEnded = false, wasSkipped = true, shouldContinue = true, toStoppedState = true)
break
@ -1075,7 +1077,7 @@ class PlaybackService : MediaSessionService() {
if (media != null) {
media.setPosition(position)
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)
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.episodeCacheSize
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.getEpisodesCount
import ac.mdiq.podcini.storage.database.Queues.getInQueueEpisodeIds
@ -84,7 +84,7 @@ object AutoCleanups {
for (item in delete) {
if (item.media == null) continue
try {
runBlocking { deleteMediaOfEpisode(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
@ -138,7 +138,7 @@ object AutoCleanups {
for (item in delete) {
if (item.media == null) continue
try {
runBlocking { deleteMediaOfEpisode(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
@ -205,7 +205,7 @@ object AutoCleanups {
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
for (item in delete) {
try {
runBlocking { deleteMediaOfEpisode(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) {
e.printStackTrace()
} 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.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Episode.Companion.BUILDING
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.Episode.PlayState
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
@ -98,7 +95,7 @@ object Episodes {
// @JvmStatic is needed because some Runnable blocks call this
@OptIn(UnstableApi::class) @JvmStatic
fun deleteMediaOfEpisode(context: Context, episode: Episode) : Job {
fun deleteEpisodeMedia(context: Context, episode: Episode) : Job {
Logd(TAG, "deleteMediaOfEpisode called ${episode.title}")
return runOnIOScope {
if (episode.media == null) return@runOnIOScope
@ -136,7 +133,7 @@ object Episodes {
url != null -> {
// delete downloaded media file
val mediaFile = File(url)
if (mediaFile.exists() && !mediaFile.delete()) {
if (!mediaFile.delete()) {
Log.e(TAG, "delete media file failed: $url")
val evt = FlowEvent.MessageEvent(context.getString(R.string.delete_failed))
EventFlow.postEvent(evt)
@ -176,6 +173,7 @@ object Episodes {
* Remove the listed episodes and their EpisodeMedia entries.
* Deleting media also removes the download log entries.
*/
@UnstableApi
fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job {
return runOnIOScope {
val removedFromQueue: MutableList<Episode> = ArrayList()
@ -290,19 +288,19 @@ object Episodes {
suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode {
Logd(TAG, "setPlayStateSync called resetMediaPosition: $resetMediaPosition")
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 {
if (it.playState == PLAYED) it.playState = UNPLAYED
else it.playState = PLAYED
if (it.playState == PlayState.PLAYED.code) it.playState = PlayState.UNPLAYED.code
else it.playState = PlayState.PLAYED.code
}
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))
return result
}
private fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
private fun shouldMarkedPlayedRemoveFromQueues(): Boolean {
return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
}
}

View File

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

View File

@ -56,7 +56,7 @@ object LogsAndStats {
feedTotalTime += m.duration
if (m.lastPlayedTime in timeFilterFrom..<timeFilterTo) {
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
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.upsertBlk
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.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
@ -144,7 +145,7 @@ object Queues {
for (event in events) EventFlow.postEvent(event)
// 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)
}
}
@ -153,7 +154,6 @@ object Queues {
suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode, queue_: PlayQueue? = null) {
Logd(TAG, "addToQueueSync( ... ) called")
// val queue = if (queue_ != null) unmanaged(queue_) else curQueue
val queue = queue_ ?: curQueue
val currentlyPlaying = curMedia
val positionCalculator = EnqueuePositionCalculator(enqueueLocation)
@ -166,11 +166,9 @@ object Queues {
insertPosition++
it.update()
}
// queueNew.episodes.addAll(queue.episodes)
// queueNew.episodes.add(insertPosition, episode)
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 (performAutoDownload) autodownloadEpisodeMedia(context)
}
@ -246,9 +244,10 @@ object Queues {
val events: MutableList<FlowEvent.QueueEvent> = ArrayList()
val indicesToRemove: MutableList<Int> = mutableListOf()
val qItems = queue.episodes.toMutableList()
val eList = episodes.toList()
for (i in qItems.indices) {
val episode = qItems[i]
if (episodes.contains(episode)) {
if (indexOfItemWithId(eList, episode.id) >= 0) {
Logd(TAG, "removing from queue: ${episode.id} ${episode.title}")
indicesToRemove.add(i)
if (queue.id == curQueue.id) events.add(FlowEvent.QueueEvent.removed(episode))

View File

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

View File

@ -86,7 +86,7 @@ class Episode : RealmObject {
@Ignore
val isNew: Boolean
get() = playState == NEW
get() = playState == PlayState.NEW.code
@Ignore
val isInProgress: Boolean
@ -123,7 +123,7 @@ class Episode : RealmObject {
}
constructor() {
this.playState = UNPLAYED
this.playState = PlayState.UNPLAYED.code
}
/**
@ -187,19 +187,19 @@ class Episode : RealmObject {
}
fun setNew() {
playState = NEW
playState = PlayState.NEW.code
}
fun isPlayed(): Boolean {
return playState == PLAYED
return playState == PlayState.PLAYED.code
}
fun setPlayed(played: Boolean) {
playState = if (played) PLAYED else UNPLAYED
playState = if (played) PlayState.PLAYED.code else PlayState.UNPLAYED.code
}
fun setBuilding() {
playState = BUILDING
playState = PlayState.BUILDING.code
}
/**
@ -252,13 +252,15 @@ class Episode : RealmObject {
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 {
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.model.FeedFunding.Companion.extractPaymentLinks
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.types.RealmList
import io.realm.kotlin.types.RealmObject
@ -144,20 +145,7 @@ class Feed : RealmObject {
@Ignore
val mostRecentItem: Episode?
get() {
// // we could sort, but we don't need to, a simple search is fine...
// var mostRecentDate = Date(0)
// var mostRecentItem: Episode? = null
// for (item in episodes) {
// val date = item.getPubDate()
// if (date != null && date.after(mostRecentDate)) {
// mostRecentDate = date
// mostRecentItem = item
// }
// }
// return mostRecentItem
return realm.query(Episode::class).query("feedId == $id SORT(pubDate DESC)").first().find()
}
get() = realm.query(Episode::class).query("feedId == $id SORT(pubDate DESC)").first().find()
@Ignore
var title: String?
@ -297,6 +285,14 @@ class Feed : RealmObject {
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 {
val TAG: String = Feed::class.simpleName ?: "Anonymous"

View File

@ -1,7 +1,9 @@
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.model.VolumeAdaptionSetting.Companion.fromInteger
import androidx.compose.runtime.mutableStateOf
import io.realm.kotlin.ext.realmSetOf
import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.RealmSet
@ -46,6 +48,8 @@ class FeedPreferences : EmbeddedRealmObject {
}
var volumeAdaption: Int = 0
var prefStreamOverDownload: Boolean = false
var filterString: String = ""
var sortOrderCode: Int = 0 // in EpisodeSortOrder
@ -62,11 +66,31 @@ class FeedPreferences : EmbeddedRealmObject {
@Ignore
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) {
field = value
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
@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.removeFromQueue
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.Companion.UNSPECIFIED
import ac.mdiq.podcini.storage.model.PlayQueue
import ac.mdiq.podcini.ui.activity.MainActivity
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.remove_from_queue_batch -> removeFromQueueChecked(items)
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)
}
// 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)
media.downloaded -> PlayActionButton(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)
}
}

View File

@ -20,7 +20,7 @@ class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
}
@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
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.playbackService
import ac.mdiq.podcini.playback.PlaybackServiceStarter
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.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
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.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
@ -57,10 +60,11 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
fun notifyMissingEpisodeMediaFile(context: Context, media: EpisodeMedia) {
Logd(TAG, "notifyMissingEpisodeMediaFile called")
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) {
val episode_ = upsertBlk(episode) {
it.media = media
// it.media = media
it.media?.downloaded = false
it.media?.fileUrl = null
}

View File

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

View File

@ -61,7 +61,7 @@ class RemoveFromQueueSwipeAction : SwipeAction {
fun addToQueueAt(episode: Episode, index: Int) : Job {
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) {
it.episodeIds.add(index, episode.id)
it.update()

View File

@ -39,7 +39,7 @@ class TogglePlaybackStateSwipeAction : SwipeAction {
}
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} )")
// 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) }
}
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
Episode.PLAYED -> R.string.marked_as_played_label
else -> if (item.playState == Episode.NEW) R.string.removed_inbox_label
Episode.PlayState.PLAYED.code -> R.string.marked_as_played_label
else -> if (item.playState == Episode.PlayState.NEW.code) R.string.removed_inbox_label
else R.string.marked_as_unplayed_label
}
val duration: Int = Snackbar.LENGTH_LONG
@ -87,7 +87,7 @@ class TogglePlaybackStateSwipeAction : SwipeAction {
}
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
}
}

View File

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

View File

@ -213,13 +213,14 @@ import java.util.*
if (nameEpisodeMap.isNotEmpty()) {
for (e in nameEpisodeMap.values) {
upsertBlk(e) {
e.media?.setfileUrlOrNull(null)
it.media?.setfileUrlOrNull(null)
}
}
}
loadItems()
Logd(TAG, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}")
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) }
episodes.addAll(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 (onInit) {
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.Logd
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.app.Activity
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.apache.commons.lang3.StringUtils
import java.lang.ref.WeakReference
import java.util.*
@ -64,8 +64,6 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private val binding get() = _binding!!
private lateinit var feed: Feed
// private lateinit var imgvCover: ImageView
// private lateinit var imgvBackground: ImageView
private lateinit var toolbar: MaterialToolbar
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) {
@ -131,6 +129,18 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
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) {
super.onConfigurationChanged(newConfig)
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() {
override fun createIntent(context: Context, input: Uri?): Intent {
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.storage.database.Feeds.persistFeedPreferences
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.model.*
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions
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.TagSettingsDialog
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 android.content.Context
import android.content.DialogInterface
@ -40,11 +38,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
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.ui.Alignment
import androidx.compose.ui.Modifier
@ -70,6 +63,7 @@ class FeedSettingsFragment : Fragment() {
private val binding get() = _binding!!
private var feed: Feed? = null
private var autoDeleteSummaryResId by mutableIntStateOf(R.string.global_default)
private var curPrefQueue by mutableStateOf(feed?.preferences?.queueTextExt ?: "Default")
private var autoDeletePolicy = "global"
private var queues: List<PlayQueue>? = null
@ -102,6 +96,7 @@ class FeedSettingsFragment : Fragment() {
var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated ?: true) }
Switch(
checked = checked,
modifier = Modifier.height(24.dp),
onCheckedChange = {
checked = it
feed = upsertBlk(feed!!) {
@ -117,6 +112,35 @@ class FeedSettingsFragment : Fragment() {
)
}
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()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
@ -125,12 +149,7 @@ class FeedSettingsFragment : Fragment() {
style = MaterialTheme.typography.h6,
color = textColor,
modifier = Modifier.clickable(onClick = {
// queues = realm.query(PlayQueue::class).find()
val selectedOption = when (feed?.preferences?.queueId) {
null, 0L -> "Default"
-1L -> "Active"
else -> "Custom"
}
val selectedOption = feed?.preferences?.queueText ?: "Default"
val composeView = ComposeView(requireContext()).apply {
setContent {
val showDialog = remember { mutableStateOf(true) }
@ -140,12 +159,11 @@ class FeedSettingsFragment : Fragment() {
}
}
(view as? ViewGroup)?.addView(composeView)
})
)
}
Text(
text = stringResource(R.string.pref_feed_associated_queue_sum),
text = curPrefQueue + " : " + stringResource(R.string.pref_feed_associated_queue_sum),
style = MaterialTheme.typography.body2,
color = textColor
)
@ -168,7 +186,6 @@ class FeedSettingsFragment : Fragment() {
}
}
(view as? ViewGroup)?.addView(composeView)
})
)
}
@ -231,12 +248,13 @@ class FeedSettingsFragment : Fragment() {
) {
Column {
FeedAutoDeleteOptions.forEach { text ->
Row(
Modifier
Row(Modifier
.fillMaxWidth()
.selectable(
selected = (text == selectedOption),
onClick = {
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = (text == selectedOption),
onCheckedChange = { isChecked ->
Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
@ -254,13 +272,6 @@ class FeedSettingsFragment : Fragment() {
}
}
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (text == selectedOption),
onClick = { }
)
Text(
text = text,
style = MaterialTheme.typography.body1.merge(),
@ -281,37 +292,36 @@ class FeedSettingsFragment : Fragment() {
var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(
modifier = Modifier
Card(modifier = Modifier
.wrapContentSize(align = Alignment.Center)
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column(
modifier = Modifier.padding(16.dp),
Column(modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
queueSettingOptions.forEach { option ->
Row(
modifier = Modifier.fillMaxWidth(),
Row(modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = option == selected,
Checkbox(checked = option == selected,
onCheckedChange = { isChecked ->
selected = option
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
"Default" -> {
feed = upsertBlk(feed!!) {
it.preferences?.queueId = 0L
}
feed = upsertBlk(feed!!) { it.preferences?.queueId = 0L }
curPrefQueue = selected
onDismissRequest()
}
"Active" -> {
feed = upsertBlk(feed!!) {
it.preferences?.queueId = 1L
feed = upsertBlk(feed!!) { it.preferences?.queueId = -1L }
curPrefQueue = selected
onDismissRequest()
}
"None" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = -2L }
curPrefQueue = selected
onDismissRequest()
}
"Custom" -> {}
@ -326,9 +336,8 @@ class FeedSettingsFragment : Fragment() {
Spinner(items = queues!!.map { it.name }, selectedItem = feed?.preferences?.queue?.name ?: "Default") { name ->
Logd(TAG, "Queue selected: $name")
val q = queues?.firstOrNull { it.name == name }
feed = upsertBlk(feed!!) {
it.preferences?.queue = q
}
feed = upsertBlk(feed!!) { it.preferences?.queue = q }
if (q != null) curPrefQueue = q.name
onDismissRequest()
}
}
@ -753,7 +762,7 @@ class FeedSettingsFragment : Fragment() {
private val TAG: String = FeedSettingsFragment::class.simpleName ?: "Anonymous"
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 {
val fragment = FeedSettingsFragment()

View File

@ -85,7 +85,7 @@ import java.util.*
private val binding get() = _binding!!
private lateinit var recyclerView: EpisodesRecyclerView
private lateinit var emptyView: EmptyViewHandler
private lateinit var emptyViewHandler: EmptyViewHandler
private lateinit var toolbar: MaterialToolbar
private lateinit var swipeActions: SwipeActions
private lateinit var speedDialView: SpeedDialView
@ -179,12 +179,12 @@ import java.util.*
adapter?.setOnSelectModeListener(this)
recyclerView.adapter = adapter
emptyView = EmptyViewHandler(requireContext())
emptyView.attachToRecyclerView(recyclerView)
emptyView.setIcon(R.drawable.ic_playlist_play)
emptyView.setTitle(R.string.no_items_header_label)
emptyView.setMessage(R.string.no_items_label)
emptyView.updateAdapter(adapter)
emptyViewHandler = EmptyViewHandler(requireContext())
emptyViewHandler.attachToRecyclerView(recyclerView)
emptyViewHandler.setIcon(R.drawable.ic_playlist_play)
emptyViewHandler.setTitle(R.string.no_items_header_label)
emptyViewHandler.setMessage(R.string.no_items_label)
emptyViewHandler.updateAdapter(adapter)
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
speedDialView = multiSelectDial.fabSD
@ -290,8 +290,12 @@ import java.util.*
}
when (event.action) {
FlowEvent.QueueEvent.Action.ADDED -> {
if (event.episodes.isNotEmpty() && !curQueue.isInQueue(event.episodes[0])) queueItems.add(event.position, event.episodes[0])
adapter?.notifyItemInserted(event.position)
if (event.episodes.isNotEmpty() && !curQueue.isInQueue(event.episodes[0])) {
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 -> {
queueItems.clear()
@ -303,9 +307,12 @@ import java.util.*
for (e in event.episodes) {
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
if (pos >= 0) {
Logd(TAG, "removing episode $pos ${queueItems[pos]} $e")
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
val holder = recyclerView.findViewHolderForLayoutPosition(pos) as? EpisodeViewHolder
holder?.unbind()
queueItems.removeAt(pos)
adapter?.notifyItemRemoved(pos)
adapter?.notifyItemRangeChanged(pos, adapter!!.getItemCount()-pos);
} else {
Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}")
continue
@ -637,18 +644,16 @@ import java.util.*
private fun loadCurQueue(restoreScrollPosition: Boolean) {
if (!loadItemsRunning) {
loadItemsRunning = true
adapter?.updateItems(mutableListOf())
// adapter?.updateItems(mutableListOf())
Logd(TAG, "loadCurQueue() called ${curQueue.name}")
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
if (queueItems.isEmpty()) emptyView.hide()
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
queueItems.clear()
if (showBin) {
queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
} else {
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)
}
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 comparator: Comparator<Feed> = when (feedOrder) {
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()
for (f in feedList_) {
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 -> {
val queryString = "feedId == $0 AND playState == ${Episode.PLAYED}"
val queryString = "feedId == $0 AND playState == ${Episode.PlayState.PLAYED.code}"
val counterMap: MutableMap<Long, Long> = mutableMapOf()
for (f in feedList_) {
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 -> {
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()
for (f in feedList_) {
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 -> {
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()
for (f in feedList_) {
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
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()
for (f in feedList_) {
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("")}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(
modifier = Modifier
Card(modifier = Modifier
.wrapContentSize(align = Alignment.Center)
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column(
modifier = Modifier.padding(16.dp),
Column(modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
queueSettingOptions.forEach { option ->
Row(
modifier = Modifier.fillMaxWidth(),
Row(modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = option == selected,
Checkbox(checked = option == selected,
onCheckedChange = { isChecked ->
selected = option
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
"Default" -> {
saveFeedPreferences { it: FeedPreferences ->
it.queueId = 0L
}
saveFeedPreferences { it: FeedPreferences -> it.queueId = 0L }
onDismissRequest()
}
"Active" -> {
saveFeedPreferences { it: FeedPreferences ->
it.queueId = -1L
saveFeedPreferences { it: FeedPreferences -> it.queueId = -1L }
onDismissRequest()
}
"None" -> {
saveFeedPreferences { it: FeedPreferences -> it.queueId = -2L }
onDismissRequest()
}
"Custom" -> {}
@ -784,9 +780,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
Logd(TAG, "Queue selected: $name")
val q = queues.firstOrNull { it.name == name }
if (q != null) {
saveFeedPreferences { it: FeedPreferences ->
it.queueId = q.id
}
saveFeedPreferences { it: FeedPreferences -> it.queueId = q.id }
onDismissRequest()
}
}

View File

@ -447,7 +447,7 @@ class StatisticsFragment : Fragment() {
else {
// progress import does not include playedDuration
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
else if (m.position > 0) dur += m.position
} else dur += m.position

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.ui.utils
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.DialogInterface
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -13,7 +13,7 @@ object LocalDeleteModal {
val localItems: MutableList<Episode> = mutableListOf()
for (item in items) {
if (item.feed?.isLocalFeed == true) localItems.add(item)
else deleteMediaOfEpisode(context, item)
else deleteEpisodeMedia(context, item)
}
if (localItems.isNotEmpty()) {
@ -22,7 +22,7 @@ object LocalDeleteModal {
.setMessage(R.string.delete_local_feed_warning_body)
.setPositiveButton(R.string.delete_label) { dialog: DialogInterface?, which: Int ->
for (item in localItems) {
deleteMediaOfEpisode(context, item)
deleteEpisodeMedia(context, item)
}
}
.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.preferences.UserPreferences
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.Companion.BUILDING
import ac.mdiq.podcini.storage.model.Episode.PlayState
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Feed.Companion.PREFIX_GENERATIVE_COVER
import ac.mdiq.podcini.storage.model.MediaType
@ -99,7 +98,6 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro
}
fun bind(item: Episode) {
// Logd(TAG, "in bind: ${item.title} ${item.isFavorite} ${item.isPlayed()}")
if (episodeMonitor == null) {
val item_ = realm.query(Episode::class).query("id == ${item.id}").first()
episodeMonitor = CoroutineScope(Dispatchers.Default).launch {
@ -116,6 +114,7 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro
else -> {}
}
}
// return
}
}
if (mediaMonitor == null) {
@ -174,7 +173,7 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro
when {
item.media != null -> bind(item.media!!)
// for generating TTS files for episode without media
item.playState == BUILDING -> {
item.playState == PlayState.BUILDING.code -> {
secondaryActionProgress.setPercentage(actionButton!!.processing, item)
secondaryActionProgress.setIndeterminate(false)
}

View File

@ -512,7 +512,7 @@
<string name="pref_feed_skip">Auto skip</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_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_intro">Skip first</string>
<string name="pref_feed_skip_ending_toast">Skipped last %d seconds</string>

View File

@ -8,7 +8,7 @@ internal object FeedItemMother {
@JvmStatic
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)
return item
}

View File

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

View File

@ -81,7 +81,7 @@ class DbNullCleanupAlgorithmTest {
feed.episodes.addAll(items)
val files: MutableList<File> = ArrayList()
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")
Assert.assertTrue(f.createNewFile())

View File

@ -33,7 +33,7 @@ class DbPlayQueueCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items)
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)
for (i in files.indices) {

View File

@ -61,7 +61,7 @@ class DbTasksTest {
feed.episodes.clear()
for (i in 0 until numItems) {
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)
@ -97,7 +97,7 @@ class DbTasksTest {
feed.episodes.clear()
for (i in 0 until numItemsOld) {
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()
// adapter.open()
@ -117,7 +117,7 @@ class DbTasksTest {
for (i in numItemsOld until numItemsNew + numItemsOld) {
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)
@ -134,7 +134,7 @@ class DbTasksTest {
@Test
fun testUpdateFeedMediaUrlResetState() {
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)
// val adapter = getInstance()
@ -166,7 +166,7 @@ class DbTasksTest {
feed.episodes.clear()
for (i in 0..9) {
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()
// adapter.open()
@ -188,7 +188,7 @@ class DbTasksTest {
feed.episodes.clear()
for (i in 0..9) {
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")
item.setMedia(media)
feed.episodes.add(item)
@ -244,7 +244,7 @@ class DbTasksTest {
val items: MutableList<Episode> = ArrayList(numFeedItems)
for (i in 1..numFeedItems) {
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)
}
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.storage.database.Episodes.addToHistory
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.getEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
@ -97,7 +97,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title")
val items: MutableList<Episode> = ArrayList()
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)
val media = EpisodeMedia(0, item, duration, 1, 1, "mime_type",
"dummy path", "download_url", true, null, 0, 0)
@ -138,7 +138,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title")
val items: MutableList<Episode> = ArrayList()
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",
dest.absolutePath, "download_url", true, null, 0, 0)
@ -154,7 +154,7 @@ class DbWriterTest {
Assert.assertTrue(item.id != 0L)
runBlocking {
val job = deleteMediaOfEpisode(context, item)
val job = deleteEpisodeMedia(context, item)
withTimeout(TIMEOUT*1000) { job.join() }
}
media = getEpisodeMedia(media.id)
@ -176,7 +176,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title")
val items: MutableList<Episode> = ArrayList()
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",
dest.absolutePath, "download_url", true, null, 0, 0)
@ -196,7 +196,7 @@ class DbWriterTest {
queue = curQueue.episodes
Assert.assertTrue(queue.size != 0)
deleteMediaOfEpisode(context, item)
deleteEpisodeMedia(context, item)
Awaitility.await().timeout(2, TimeUnit.SECONDS).until { !dest.exists() }
media = getEpisodeMedia(media.id)
Assert.assertNotNull(media)
@ -218,7 +218,7 @@ class DbWriterTest {
val itemFiles: MutableList<File> = ArrayList()
// create items with downloaded media files
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)
val enc = File(destFolder, "file $i")
@ -308,7 +308,7 @@ class DbWriterTest {
// create items
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)
}
@ -352,7 +352,7 @@ class DbWriterTest {
// create items with downloaded media files
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)
val enc = File(destFolder, "file $i")
val media = EpisodeMedia(0, item, 1, 1, 1, "mime_type",
@ -415,7 +415,7 @@ class DbWriterTest {
// create items with downloaded media files
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)
val enc = File(destFolder, "file $i")
val media = EpisodeMedia(0, item, 1, 1, 1, "mime_type",
@ -463,7 +463,7 @@ class DbWriterTest {
// create items
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, ""))
feed.episodes.add(item)
}
@ -494,7 +494,7 @@ class DbWriterTest {
private fun playbackHistorySetup(playbackCompletionDate: Date?): EpisodeMedia {
val feed = Feed("url", null, "title")
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,
"url", false, playbackCompletionDate, 0, 0)
feed.episodes.add(item)
@ -547,7 +547,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title")
feed.episodes.clear()
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, ""))
feed.episodes.add(item)
}
@ -576,7 +576,7 @@ class DbWriterTest {
fun testAddQueueItemSingleItem() {
val feed = Feed("url", null, "title")
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, ""))
feed.episodes.add(item)
@ -605,7 +605,7 @@ class DbWriterTest {
fun testAddQueueItemSingleItemAlreadyInQueue() {
val feed = Feed("url", null, "title")
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, ""))
feed.episodes.add(item)
@ -766,7 +766,7 @@ class DbWriterTest {
feed.episodes.clear()
for (i in 0 until numItems) {
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, ""))
feed.episodes.add(item)
}
@ -817,7 +817,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title")
feed.episodes.clear()
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, ""))
feed.episodes.add(item)
}
@ -848,7 +848,7 @@ class DbWriterTest {
val feed = Feed("url", null, "title")
feed.episodes.clear()
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, ""))
feed.episodes.add(item)
}

View File

@ -57,7 +57,7 @@ class EpisodeDuplicateGuesserTest {
private fun item(guid: String, title: String, downloadUrl: String,
date: Long, duration: Long, mime: String
): 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)
item.setMedia(media)
return item

View File

@ -29,7 +29,7 @@ class ExceptFavoriteCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items)
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)
for (i in files.indices) {
@ -48,7 +48,7 @@ class ExceptFavoriteCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items)
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)
for (i in files.indices) {
@ -67,7 +67,7 @@ class ExceptFavoriteCleanupAlgorithmTest : DbCleanupTests() {
val items: MutableList<Episode> = ArrayList()
feed.episodes.addAll(items)
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)
for (i in files.indices) {

View File

@ -57,7 +57,7 @@ object ItemEnqueuePositionCalculatorTest {
fun createFeedItem(id: Long): Episode {
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")
media.id = item.id
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
* 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