diff --git a/README.md b/README.md index d0b62879..20fc089f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c ## Notable new features & enhancements -### Player +### Player and Queues * More convenient player control displayed on all pages * Revamped and more efficient expanded player view showing episode description on the front @@ -59,6 +59,14 @@ 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 +* 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 + ### Podcast/Episode list @@ -82,11 +90,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * on action bar of FeedEpisodes view there is a direct access to Queue * Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings * History view shows time of last play, and allows filters and sorts -* Multiple queues can be used: 5 queues are provided by default, user can add up to 10 queues - * on app startup, the most recently updated queue is set to curQueue -* 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 - ### Podcast/Episode * New share notes menu option on various episode views @@ -121,11 +124,12 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. "newest episodes" meaning most recent episodes, new or old) * Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app. * Auto downloads run feeds or feed refreshes, scheduled or manual - * auto download always includes any undownloaded episodes (regardless of feeds) added in the current queue + * auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue * After auto download run, episodes with New status is changed to Unplayed. * auto download feed setting dialog is also changed: * there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently * on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played" +* Sleep timer has a new option of "To the end of episode" ### Security and reliability diff --git a/app/build.gradle b/app/build.gradle index ab733e3a..e3f7e0c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,17 +25,14 @@ android { kotlinOptions { jvmTarget = '17' } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.14" - } vectorDrawables.useSupportLibrary false vectorDrawables.generatedDensities = [] testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020222 - versionName "6.2.1" + versionCode 3020223 + versionName "6.2.2" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt index b543efc5..67349794 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt @@ -44,7 +44,7 @@ object DownloadRequestCreator { Logd(TAG, "Requesting download media from url " + media.downloadUrl) - val feed = media.episode?.feed + val feed = media.episodeOrFetch()?.feed val username = feed?.preferences?.username val password = feed?.preferences?.password diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index f8b1276a..62e772e9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -75,7 +75,8 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { @OptIn(UnstableApi::class) override fun cancel(context: Context, media: EpisodeMedia) { Logd(TAG, "starting cancel") // This needs to be done here, not in the worker. Reason: The worker might or might not be running. - if (media.episode != null) Episodes.deleteMediaOfEpisode(context, media.episode!!) // Remove partially downloaded file + val item_ = media.episodeOrFetch() + if (item_ != null) Episodes.deleteMediaOfEpisode(context, item_) // Remove partially downloaded file val tag = WORK_TAG_EPISODE_URL + media.downloadUrl val future: Future> = WorkManager.getInstance(context).getWorkInfosByTag(tag) @@ -83,8 +84,10 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { try { val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result workInfoList.forEach { workInfo -> +// TODO: why cancel so many times?? if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) { - if (media.episode != null) Queues.removeFromQueue(media.episode!!) + val item_ = media.episodeOrFetch() + if (item_ != null) Queues.removeFromQueue(item_) } } WorkManager.getInstance(context).cancelAllWorkByTag(tag) @@ -202,6 +205,10 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { @OptIn(UnstableApi::class) private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result { Logd(TAG, "starting performDownload") + if (request.destination == null) { + Log.e(TAG, "performDownload request.destination is null") + return Result.failure() + } val dest = File(request.destination) if (!dest.exists()) { try { @@ -338,17 +345,19 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { return } // media.setDownloaded modifies played state - val broadcastUnreadStateUpdate = media.episode != null && media.episode!!.isNew + var item_ = media.episodeOrFetch() + val broadcastUnreadStateUpdate = item_?.isNew == true // media.downloaded = true media.setIsDownloaded() - Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}") + item_ = media.episodeOrFetch() + Logd(TAG, "media.episode.isNew: ${item_?.isNew} ${item_?.playState}") media.setfileUrlOrNull(request.destination) if (request.destination != null) media.size = File(request.destination).length() media.checkEmbeddedPicture() // enforce check // check if file has chapters - if (media.episode != null && media.episode!!.chapters.isEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context)) - if (media.episode?.podcastIndexChapterUrl != null) - ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false) + if (item_?.chapters.isNullOrEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context)) + if (item_?.podcastIndexChapterUrl != null) + ChapterUtils.loadChaptersFromUrl(item_.podcastIndexChapterUrl!!, false) // Get duration var durationStr: String? = null try { @@ -364,7 +373,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { Log.e(TAG, "Get duration failed", e) media.setDuration(30000) } - val item = media.episode + val item = media.episodeOrFetch() item?.media = media try { // we've received the media, we don't want to autodownload it again diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt index d712fa23..5c85f6d5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt @@ -62,9 +62,10 @@ object SynchronizationQueueSink { fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) { if (!isProviderConnected) return - if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return + val item_ = media.episodeOrFetch() + if (item_?.feed?.isLocalFeed == true) return if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return - val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY) + val action = EpisodeAction.Builder(item_!!, EpisodeAction.PLAY) .currentTimestamp() .started(media.startPosition / 1000) .position((if (completed) media.getDuration() else media.getPosition()) / 1000) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt index 97f6ae78..2f3d3772 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt @@ -48,7 +48,7 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl if (media is EpisodeMedia) { curMedia = media // curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null - curEpisode = media.episode + curEpisode = media.episodeOrFetch() // curMedia = curEpisode?.media } else curMedia = media diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt index a03b3099..8b82e42f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt @@ -19,20 +19,23 @@ import kotlinx.coroutines.* object InTheatre { val TAG: String = InTheatre::class.simpleName ?: "Anonymous" + var curIndexInQueue = -1 + var curQueue: PlayQueue // unmanaged var curEpisode: Episode? = null // unmanged set(value) { - field = value - if (curMedia != field?.media) curMedia = field?.media + field = if (value != null) unmanaged(value) else null + if (field?.media != null && curMedia?.getIdentifier() != field?.media?.getIdentifier()) curMedia = unmanaged(field!!.media!!) } var curMedia: Playable? = null // unmanged if EpisodeMedia set(value) { - field = if (value != null && value is EpisodeMedia) unmanaged(value) else value - if (field is EpisodeMedia) { - val media = (field as EpisodeMedia) - if (curEpisode != media.episode) curEpisode = media.episode + if (value is EpisodeMedia) { + field = unmanaged(value) + if (value.episode != null && curEpisode?.id != value.episode?.id) curEpisode = unmanaged(value.episode!!) + } else { + field = value } } @@ -115,7 +118,7 @@ object InTheatre { val mediaId = curState.curMediaId if (mediaId != 0L) { curMedia = getEpisodeMedia(mediaId) - if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episode + if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episodeOrFetch() } } else Log.e(TAG, "Could not restore Playable object from preferences") } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index 9cbaceeb..8e42c059 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -346,7 +346,8 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont if (media != null) { playbackSpeed = curState.curTempSpeed if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) { - if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed + val prefs_ = media.episodeOrFetch()?.feed?.preferences + if (prefs_ != null) playbackSpeed = prefs_.playSpeed } } if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = getPlaybackSpeed(mediaType) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index d459c273..4f258cdc 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -6,7 +6,9 @@ import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder import ac.mdiq.podcini.net.download.service.PodciniHttpClient import ac.mdiq.podcini.net.utils.NetworkUtils.wasDownloadBlocked import ac.mdiq.podcini.playback.base.InTheatre.curEpisode +import ac.mdiq.podcini.playback.base.InTheatre.curIndexInQueue import ac.mdiq.podcini.playback.base.InTheatre.curMedia +import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerCallback import ac.mdiq.podcini.playback.base.PlayerStatus @@ -15,11 +17,13 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.MediaType +import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.config.ClientConfig import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent.PlayEvent.Action +import ac.mdiq.podcini.util.showStackTrace import android.app.UiModeManager import android.content.Context import android.content.res.Configuration @@ -225,11 +229,18 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP */ override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) { Logd(TAG, "playMediaObject status=$status stream=$stream startWhenPrepared=$startWhenPrepared prepareImmediately=$prepareImmediately forceReset=$forceReset ${playable.getEpisodeTitle()} ") +// showStackTrace() if (curMedia != null) { + Logd(TAG, "playMediaObject: curMedia exist status=$status") if (!forceReset && curMedia!!.getIdentifier() == prevMedia?.getIdentifier() && status == PlayerStatus.PLAYING) { Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.") return } + if (curMedia is EpisodeMedia) { + val media_ = curMedia as EpisodeMedia + curIndexInQueue = EpisodeUtil.indexOfItemWithId(curQueue.episodes, media_.id) + } else curIndexInQueue = -1 + Logd(TAG, "playMediaObject starts new media playable:${playable.getIdentifier()} curMedia:${curMedia!!.getIdentifier()} prevMedia:${prevMedia?.getIdentifier()}") // set temporarily to pause in order to update list with current position if (status == PlayerStatus.PLAYING) { @@ -241,12 +252,12 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP if (status == PlayerStatus.PAUSED || status == PlayerStatus.PLAYING || status == PlayerStatus.PREPARED) exoPlayer?.stop() // if (prevMedia != null && curMedia!!.getIdentifier() != prevMedia?.getIdentifier()) // callback.onPostPlayback(prevMedia, ended = false, skipped = false, true) - prevMedia = curMedia setPlayerStatus(PlayerStatus.INDETERMINATE, null) } Logd(TAG, "playMediaObject preparing for playable:${playable.getIdentifier()} ${playable.getEpisodeTitle()}") curMedia = playable + prevMedia = curMedia this.isStreaming = stream mediaType = curMedia!!.getMediaType() videoSize = null @@ -264,7 +275,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP if (streamurl != null) { val media = curMedia if (media is EpisodeMedia) { - val preferences = media.episode?.feed?.preferences + val preferences = media.episodeOrFetch()?.feed?.preferences setDataSource(metadata, streamurl, preferences?.username, preferences?.password) } else setDataSource(metadata, streamurl, null, null) } @@ -289,6 +300,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP e.printStackTrace() setPlayerStatus(PlayerStatus.ERROR, null) EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: "")) + } finally { } } @@ -431,13 +443,15 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP var volumeLeft = volumeLeft var volumeRight = volumeRight val playable = curMedia - if (playable is EpisodeMedia && playable.episode?.feed?.preferences != null) { - val preferences = playable.episode!!.feed!!.preferences!! - val volumeAdaptionSetting = preferences.volumeAdaptionSetting - if (volumeAdaptionSetting != null) { - val adaptionFactor = volumeAdaptionSetting.adaptionFactor - volumeLeft *= adaptionFactor - volumeRight *= adaptionFactor + if (playable is EpisodeMedia) { + val preferences = playable.episodeOrFetch()?.feed?.preferences + if (preferences != null) { + val volumeAdaptionSetting = preferences.volumeAdaptionSetting + if (volumeAdaptionSetting != null) { + val adaptionFactor = volumeAdaptionSetting.adaptionFactor + volumeLeft *= adaptionFactor + volumeRight *= adaptionFactor + } } } if (volumeLeft > 1) { @@ -532,6 +546,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) { releaseWifiLockIfNecessary() + if (curMedia == null) return val isPlaying = status == PlayerStatus.PLAYING // we're relying on the position stored in the Playable object for post-playback processing @@ -566,7 +581,8 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP else Logd(TAG, "Ignored call to stop: Current player state is: $status") } val hasNext = nextMedia != null - callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext) + if (currentMedia != null) callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext) +// curMedia = nextMedia } isPlaying -> callback.onPlaybackPause(currentMedia, currentMedia!!.getPosition()) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index 453fbec7..42615ec8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -7,6 +7,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.base.InTheatre.curEpisode +import ac.mdiq.podcini.playback.base.InTheatre.curIndexInQueue import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curState @@ -36,7 +37,6 @@ import ac.mdiq.podcini.storage.database.Episodes.persistEpisode import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem -import ac.mdiq.podcini.storage.database.Queues.addToQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.unmanaged @@ -47,6 +47,7 @@ import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_OTHER import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PAUSED import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYING import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction +import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState @@ -283,7 +284,7 @@ class PlaybackService : MediaSessionService() { // TODO: test // return } - var item = (playable as? EpisodeMedia)?.episode ?: currentitem + var item = (playable as? EpisodeMedia)?.episodeOrFetch() ?: currentitem val smartMarkAsPlayed = hasAlmostEnded(playable) if (!ended && smartMarkAsPlayed) Logd(TAG, "smart mark as played") @@ -350,12 +351,17 @@ 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() return null } - val item = currentMedia.episode + val item = currentMedia.episodeOrFetch() if (item == null) { Logd(TAG, "getNextInQueue() with EpisodeMedia object whose FeedItem is null") writeNoMediaPlaying() @@ -367,13 +373,20 @@ class PlaybackService : MediaSessionService() { writeNoMediaPlaying() return null } - val i = curQueue.episodes.indexOf(item) - if (i < 0) { - writeNoMediaPlaying() - return null - } var j = 0 - if (i < curQueue.episodes.size-1) j = i+1 + val i = EpisodeUtil.indexOfItemWithId(curQueue.episodes, item.id) + if (i < 0) { + if (curIndexInQueue >= 0) { + if (curIndexInQueue < curQueue.episodes.size) j = curIndexInQueue + else j = curQueue.episodes.size-1 + } + else { + Logd(TAG, "getNextInQueue curMedia is not in queue ${item.title}") + writeNoMediaPlaying() + return null + } + } else if (i < curQueue.episodes.size-1) j = i+1 + val nextItem = unmanaged(curQueue.episodes[j]) if (nextItem.media == null) { Logd(TAG, "getNextInQueue nextItem: $nextItem media is null") @@ -388,6 +401,7 @@ class PlaybackService : MediaSessionService() { } if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) { + Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}") displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent) writeNoMediaPlaying() return null @@ -441,7 +455,7 @@ class PlaybackService : MediaSessionService() { curState.curMediaType = playable.getPlayableType().toLong() curState.curIsVideo = playable.getMediaType() == MediaType.VIDEO if (playable is EpisodeMedia) { - val feedId = playable.episode?.feed?.id + val feedId = playable.episodeOrFetch()?.feed?.id if (feedId != null) curState.curFeedId = feedId curState.curMediaId = playable.id } else { @@ -734,7 +748,7 @@ class PlaybackService : MediaSessionService() { } private fun skipIntro(playable: Playable) { - val item = (playable as? EpisodeMedia)?.episode ?: currentitem ?: return + val item = (playable as? EpisodeMedia)?.episodeOrFetch() ?: currentitem ?: return val feed = item.feed val preferences = feed?.preferences @@ -911,7 +925,7 @@ class PlaybackService : MediaSessionService() { mPlayer?.playMediaObject(media, stream, startWhenPrepared = true, true) recreateMediaSessionIfNeeded() - val episode = (media as? EpisodeMedia)?.episode +// val episode = (media as? EpisodeMedia)?.episode // if (curMedia is EpisodeMedia && episode != null) addToQueue(true, episode) } @@ -963,6 +977,7 @@ class PlaybackService : MediaSessionService() { if (event.action == FlowEvent.QueueEvent.Action.REMOVED) { for (e in event.episodes) { if (e.id == curEpisode?.id) { + Logd(TAG, "onQueueEvent: ending playback ${curEpisode?.title}") mPlayer?.endPlayback(hasEnded = false, wasSkipped = true, shouldContinue = true, toStoppedState = true) break } @@ -975,7 +990,7 @@ class PlaybackService : MediaSessionService() { // } private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) { - val item = (curMedia as? EpisodeMedia)?.episode ?: currentitem + val item = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: currentitem if (item?.feed?.id == event.feed.id) { item.feed = null // seems no need to pause?? @@ -1028,7 +1043,7 @@ class PlaybackService : MediaSessionService() { private fun skipEndingIfNecessary() { val remainingTime = curDuration - curPosition - val item = (curMedia as? EpisodeMedia)?.episode ?: currentitem ?: return + val item = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: currentitem ?: return val skipEnd = item.feed?.preferences?.endingSkip?:0 val skipEndMS = skipEnd * 1000 @@ -1061,7 +1076,7 @@ class PlaybackService : MediaSessionService() { playable.setLastPlayedTime(System.currentTimeMillis()) if (playable is EpisodeMedia) { - val item = playable.episode + val item = playable.episodeOrFetch() if (item != null && item.isNew) item.playState = Episode.UNPLAYED if (playable.startPosition >= 0 && playable.getPosition() > playable.startPosition) playable.playedDuration = (playable.playedDurationWhenStarted + playable.getPosition() - playable.startPosition) @@ -1249,8 +1264,9 @@ class PlaybackService : MediaSessionService() { fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) { val playable = curMedia if (playable is EpisodeMedia) { - if (playable.episode?.feed?.id == feedId) { - playable.episode!!.feed!!.preferences?.volumeAdaptionSetting = volumeAdaptionSetting + val item_ = playable.episodeOrFetch() + if (item_?.feed?.id == feedId) { + item_.feed!!.preferences?.volumeAdaptionSetting = volumeAdaptionSetting if (MediaPlayerBase.status == PlayerStatus.PLAYING) { mediaPlayer.pause(abandonFocus = false, reinit = false) mediaPlayer.resume() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/TaskManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/TaskManager.kt index d3b8acda..015728f9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/TaskManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/TaskManager.kt @@ -56,8 +56,7 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba */ @get:Synchronized val isSleepTimerActive: Boolean - get() = (sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture!!.isCancelled - && !sleepTimerFuture!!.isDone) && sleepTimer!!.getWaitingTime() > 0 + get() = sleepTimerFuture?.isCancelled == false && sleepTimerFuture?.isDone == false && (sleepTimer?.getWaitingTime() ?: 0) > 0 /** * Returns the current sleep timer time or 0 if the sleep timer is not active. @@ -248,7 +247,7 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.updated(timeLeft)) while (timeLeft > 0) { try { - Thread.sleep(UPDATE_INTERVAL) + Thread.sleep(SLEEP_TIMER_UPDATE_INTERVAL) } catch (e: InterruptedException) { Logd(TAG, "Thread was interrupted while waiting") e.printStackTrace() @@ -344,16 +343,12 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba companion object { private val TAG: String = TaskManager::class.simpleName ?: "Anonymous" - /** - * Update interval of position saver in milliseconds. - */ - const val POSITION_SAVER_WAITING_INTERVAL: Int = 5000 - /** - * Notification interval of widget updater in milliseconds. - */ - const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 5000 + private const val SCHED_EX_POOL_SIZE = 2 - private const val UPDATE_INTERVAL = 1000L - const val NOTIFICATION_THRESHOLD: Long = 10000 + + private const val SLEEP_TIMER_UPDATE_INTERVAL = 10000L // in millisoconds + const val POSITION_SAVER_WAITING_INTERVAL: Int = 5000 // in millisoconds + const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 5000 // in millisoconds + const val NOTIFICATION_THRESHOLD: Long = 10000 // in millisoconds } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt index 09bd0a12..aef185a8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt @@ -36,12 +36,12 @@ object SleepTimerPreferences { } @JvmStatic - fun setLastTimer(value: String?) { + fun setLastTimer(value: String?) { // in minutes prefs!!.edit().putString(Prefs.LastValue.name, value).apply() } @JvmStatic - fun lastTimerValue(): String? { + fun lastTimerValue(): String? { // in minutes return prefs!!.getString(Prefs.LastValue.name, DEFAULT_LAST_TIMER) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt index fe3a0c11..b1764b80 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -4,6 +4,7 @@ import ac.mdiq.podcini.storage.model.ProxyConfig import ac.mdiq.podcini.storage.utils.FilesUtils import ac.mdiq.podcini.storage.utils.FilesUtils.createNoMediaFile import ac.mdiq.podcini.util.Logd +import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.os.Build @@ -18,6 +19,7 @@ import java.net.Proxy * init() or otherwise every public method will throw an Exception * when called. */ +@SuppressLint("StaticFieldLeak") object UserPreferences { private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 11e8c11e..2d593406 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -222,7 +222,7 @@ object Episodes { fun persistEpisodeMedia(media: EpisodeMedia) : Job { Logd(TAG, "persistEpisodeMedia called") return runOnIOScope { - var episode = media.episode + var episode = media.episodeOrFetch() if (episode != null) { episode.media = media episode = upsert(episode) {} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt index 88b4e447..55321feb 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt @@ -40,7 +40,7 @@ object LogsAndStats { Logd(TAG, "getStatistics called") val medias = realm.query(EpisodeMedia::class).find() - val groupdMedias = medias.groupBy { it.episode?.feedId ?: 0L } + val groupdMedias = medias.groupBy { it.episodeOrFetch()?.feedId ?: 0L } val result = StatisticsResult() result.oldestDate = Long.MAX_VALUE for ((fid, feedMedias) in groupdMedias) { @@ -56,7 +56,7 @@ object LogsAndStats { feedTotalTime += m.duration if (m.lastPlayedTime in timeFilterFrom.. 0 && m.playedDuration > 0) || m.episode?.playState == Episode.PLAYED || m.position > 0) { + if ((m.playbackCompletionTime > 0 && m.playedDuration > 0) || m.episodeOrFetch()?.playState == Episode.PLAYED || m.position > 0) { episodesStarted += 1 feedPlayedTime += m.duration } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index a6208f5b..7a4d984a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -216,13 +216,14 @@ object Queues { @OptIn(UnstableApi::class) fun removeFromAllQueuesSync(vararg episodes: Episode) { - Logd(TAG, "removeFromAllQueues called ") + Logd(TAG, "removeFromAllQueuesSync called ") val queues = realm.query(PlayQueue::class).find() for (q in queues) { if (q.id != curQueue.id) removeFromQueueSync(q, *episodes) } // ensure curQueue is last updated - removeFromQueueSync(curQueue, *episodes) + if (curQueue.size() > 0) removeFromQueueSync(curQueue, *episodes) + else upsertBlk(curQueue) { it.update() } } /** @@ -232,8 +233,9 @@ object Queues { internal fun removeFromQueueSync(queue_: PlayQueue?, vararg episodes: Episode) { Logd(TAG, "removeFromQueueSync called ") if (episodes.isEmpty()) return - var queue = queue_ ?: curQueue + if (queue.size() == 0) return + val events: MutableList = ArrayList() val indicesToRemove: MutableList = mutableListOf() val qItems = queue.episodes.toMutableList() @@ -269,7 +271,7 @@ object Queues { var idsInQueuesToRemove: MutableSet val queues = realm.query(PlayQueue::class).find() for (q in queues) { - if (q.id == curQueue.id) continue + if (q.size() == 0 || q.id == curQueue.id) continue idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() if (idsInQueuesToRemove.isNotEmpty()) { q.idsBinList.removeAll(idsInQueuesToRemove) @@ -284,6 +286,10 @@ object Queues { } // ensure curQueue is last updated val q = curQueue + if (q.size() == 0) { + upsert(q) { it.update() } + return + } idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() if (idsInQueuesToRemove.isNotEmpty()) { q.idsBinList.removeAll(idsInQueuesToRemove) @@ -374,7 +380,7 @@ object Queues { } private fun getCurrentlyPlayingPosition(queueItems: List, currentPlaying: Playable?): Int { if (currentPlaying !is EpisodeMedia) return -1 - val curPlayingItemId = currentPlaying.episode!!.id + val curPlayingItemId = currentPlaying.episodeOrFetch()?.id for (i in queueItems.indices) { if (curPlayingItemId == queueItems[i].id) return i } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index f097d252..6c35488b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -10,7 +10,6 @@ import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.ext.isManaged -import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.TypedRealmObject import kotlinx.coroutines.* @@ -19,7 +18,7 @@ import kotlin.coroutines.ContinuationInterceptor object RealmDB { private val TAG: String = RealmDB::class.simpleName ?: "Anonymous" - private const val SCHEMA_VERSION_NUMBER = 16L + private const val SCHEMA_VERSION_NUMBER = 17L private val ioScope = CoroutineScope(Dispatchers.IO) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt index 6fb097e8..d8c72c2e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt @@ -1,12 +1,15 @@ package ac.mdiq.podcini.storage.model import ac.mdiq.podcini.storage.database.RealmDB.realm +import ac.mdiq.podcini.storage.database.RealmDB.unmanaged +import ac.mdiq.podcini.storage.database.RealmDB.update import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat import ac.mdiq.podcini.util.Logd import android.content.Context import android.os.Parcel import android.os.Parcelable +import io.realm.kotlin.ext.isManaged import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.annotations.Ignore import io.realm.kotlin.types.annotations.Index @@ -38,11 +41,11 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { @get:JvmName("getDurationProperty") @set:JvmName("setDurationProperty") - var duration = 0 + var duration = 0 // in milliseconds @get:JvmName("getPositionProperty") @set:JvmName("setPositionProperty") - var position = 0 // Current position in file + var position = 0 // Current position in file, in milliseconds @get:JvmName("getLastPlayedTimeProperty") @set:JvmName("setLastPlayedTimeProperty") @@ -58,7 +61,6 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { var episode: Episode? = null - var playbackCompletionTime: Long = 0 @Ignore var playbackCompletionDate: Date? = null get() = field?.clone() as? Date @@ -66,6 +68,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { field = value?.clone() as? Date this.playbackCompletionTime = value?.time ?: 0 } + var playbackCompletionTime: Long = 0 var startPosition: Int = -1 @@ -76,8 +79,8 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { var hasEmbeddedPicture: Boolean? = null /* Used for loading item when restoring from parcel. */ - var episodeId: Long = 0 - private set +// var episodeId: Long = 0 +// private set @Ignore val isInProgress: Boolean @@ -116,7 +119,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { } fun getHumanReadableIdentifier(): String? { - return if (episode?.title != null) episode!!.title else downloadUrl + return episode?.title ?: downloadUrl } /** @@ -175,7 +178,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { override fun setPosition(newPosition: Int) { this.position = newPosition - if (newPosition > 0 && episode != null && episode!!.isNew) episode!!.setPlayed(false) + if (newPosition > 0 && episode?.isNew == true) episode!!.setPlayed(false) } override fun getLastPlayedTime(): Long { @@ -236,18 +239,6 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { dest.writeLong(lastPlayedTime) } -// no longer needed -// override fun writeToPreferences(prefEditor: SharedPreferences.Editor) { -// if (episode == null) prefEditor.putLong(PREF_FEED_ID, 0L) -// else { -// val f = episode!!.feed -// if (f != null) prefEditor.putLong(PREF_FEED_ID, f.id) -// else prefEditor.putLong(PREF_FEED_ID, 0L) -// } -// -// prefEditor.putLong(PREF_MEDIA_ID, id) -// } - override fun getEpisodeTitle(): String { return episode?.title ?: episode?.identifyingValue ?: "No title" } @@ -364,12 +355,22 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { result = 31 * result + startPosition result = 31 * result + playedDurationWhenStarted result = 31 * result + (hasEmbeddedPicture?.hashCode() ?: 0) - result = 31 * result + episodeId.hashCode() +// result = 31 * result + episodeId.hashCode() return result } - fun getTheEpisode(): Episode? { - return if (episode != null) episode else realm.query(Episode::class).query("id == $id").first().find() + fun episodeOrFetch(): Episode? { + return if (episode != null) episode else { + var item = realm.query(Episode::class).query("id == $id").first().find() + Logd(TAG, "episodeOrFetch warning: episode of media is null: ${id} ${item?.title}") + if (item != null) { + item = upsertBlk(item) { + it.media = this@EpisodeMedia + it.media!!.episode = it + } + } + if (item == null || isManaged()) item else unmanaged(item) + } } companion object { @@ -408,7 +409,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { Date(inVal.readLong()), inVal.readInt(), inVal.readLong()) - result.episodeId = itemID +// result.episodeId = itemID return result } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt index 0e284bb3..133ecea7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt @@ -32,5 +32,9 @@ class PlayQueue : RealmObject { updated = Date().time } + fun size() : Int { + return episodeIds.size + } + constructor() {} } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt index 39c03d05..a02c3225 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt @@ -47,7 +47,7 @@ object ChapterUtils { var chaptersFromDatabase: List? = null var chaptersFromPodcastIndex: List? = null if (playable is EpisodeMedia) { - val item = playable.episode + val item = playable.episodeOrFetch() if (item != null) { if (item.chapters.isNotEmpty()) chaptersFromDatabase = item.chapters if (!item.podcastIndexChapterUrl.isNullOrEmpty()) chaptersFromPodcastIndex = loadChaptersFromUrl(item.podcastIndexChapterUrl!!, forceRefresh) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FilesUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FilesUtils.kt index ec906aae..4f6b644f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FilesUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/FilesUtils.kt @@ -4,6 +4,7 @@ import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.util.Logd +import android.annotation.SuppressLint import android.content.Context import android.util.Log import android.webkit.URLUtil @@ -12,6 +13,7 @@ import org.apache.commons.io.FilenameUtils import java.io.File import java.io.IOException +@SuppressLint("StaticFieldLeak") object FilesUtils { private val TAG: String = FilesUtils::class.simpleName ?: "Anonymous" @@ -47,9 +49,9 @@ object FilesUtils { } fun getMediafilePath(media: EpisodeMedia): String { - val item = media.getTheEpisode() ?: return "" - Logd(TAG, "item managed: ${item?.isManaged()}") - val title = item?.feed?.title?:return "" + val item = media.episodeOrFetch() ?: return "" + Logd(TAG, "item managed: ${item.isManaged()}") + val title = item.feed?.title?:return "" val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title)) return getDataFolder(mediaPath).toString() + "/" } @@ -58,10 +60,8 @@ object FilesUtils { var titleBaseFilename = "" // Try to generate the filename by the item title - if (media.episode?.title != null) { - val title = media.episode!!.title!! - titleBaseFilename = FileNameGenerator.generateFileName(title) - } + val item_ = media.episodeOrFetch() + if (item_?.title != null) titleBaseFilename = FileNameGenerator.generateFileName(item_.title!!) val urlBaseFilename = URLUtil.guessFileName(media.downloadUrl, null, media.mimeType) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt index 1b9af649..41ab89d0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt @@ -38,7 +38,7 @@ object ImageResourceUtils { @JvmStatic fun getFallbackImageLocation(playable: Playable): String? { if (playable is EpisodeMedia) { - val item = playable.episode + val item = playable.episodeOrFetch() return item?.feed?.imageUrl } else return playable.getImageLocation() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt index 0b70d99b..cd721ac2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt @@ -5,6 +5,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying +import ac.mdiq.podcini.util.Logd import android.content.Context import android.view.View import android.widget.ImageView @@ -38,7 +39,7 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis null -> false else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false } -// Logd("ItemActionButton", "forItem: ${episode.feedId} ${episode.feed?.isLocalFeed} ${media.downloaded} ${isCurrentlyPlaying(media)} ${curMedia is EpisodeMedia} ${media.id == (curMedia as? EpisodeMedia)?.id} ${episode.title} ") +// Logd("ItemActionButton", "forItem: ${episode.feedId} ${episode.feed?.isLocalFeed} ${media.downloaded} ${isCurrentlyPlaying(media)} ${episode.title} ") return when { media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(episode) isCurrentlyPlaying(media) -> PauseActionButton(episode) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt index ad5fdac5..34177d59 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt @@ -57,7 +57,7 @@ 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.episode + val episode = media.episodeOrFetch() if (episode != null) { episode.media = media episode.media?.downloaded = false diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index 9b8a3298..a761cba5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -232,7 +232,7 @@ class VideoplayerActivity : CastEnabledActivity() { val hasWebsiteLink = getWebsiteLinkWithFallback(media) != null menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink) - val isItemAndHasLink = isEpisodeMedia && hasLinkToShare((media as EpisodeMedia).episode) + val isItemAndHasLink = isEpisodeMedia && hasLinkToShare((media as EpisodeMedia).episodeOrFetch()) val isItemHasDownloadLink = isEpisodeMedia && (media as EpisodeMedia?)?.downloadUrl != null menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink) @@ -287,7 +287,7 @@ class VideoplayerActivity : CastEnabledActivity() { // controller == null -> return false else -> { val media = curMedia ?: return false - val feedItem = (media as? EpisodeMedia)?.episode + val feedItem = (media as? EpisodeMedia)?.episodeOrFetch() when { item.itemId == R.id.add_to_favorites_item && feedItem != null -> { setFavorite(feedItem, true) @@ -470,7 +470,7 @@ class VideoplayerActivity : CastEnabledActivity() { return when { media == null -> null !media.getWebsiteLink().isNullOrBlank() -> media.getWebsiteLink() - media is EpisodeMedia -> media.episode?.getLinkWithFallback() + media is EpisodeMedia -> media.episodeOrFetch()?.getLinkWithFallback() else -> null } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt index 98f23f6d..21e7a2cf 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt @@ -42,7 +42,7 @@ abstract class EpisodeFilterDialog : BottomSheetDialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val layout = inflater.inflate(R.layout.filter_dialog, null, false) + val layout = inflater.inflate(R.layout.filter_dialog, container, false) _binding = FilterDialogBinding.bind(layout) rows = binding.filterRows Logd("EpisodeFilterDialog", "fragment onCreateView") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedFilterDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedFilterDialog.kt index d0b6dcc9..88b2f7c0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedFilterDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedFilterDialog.kt @@ -46,7 +46,7 @@ class FeedFilterDialog : BottomSheetDialogFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - val layout = inflater.inflate(R.layout.filter_dialog, null, false) + val layout = inflater.inflate(R.layout.filter_dialog, container, false) _binding = FilterDialogBinding.bind(layout) rows = binding.filterRows Logd("FeedFilterDialog", "fragment onCreateView") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt index 074ae998..1b7b4266 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt @@ -2,7 +2,9 @@ package ac.mdiq.podcini.ui.dialog import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.TimeDialogBinding +import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService +import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom @@ -21,6 +23,7 @@ import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.TAG import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong +import ac.mdiq.podcini.storage.utils.TimeSpeedConverter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent @@ -46,19 +49,15 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.util.* -import kotlin.math.abs -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.sin +import java.util.concurrent.TimeUnit +import kotlin.math.* +import kotlin.time.DurationUnit class SleepTimerDialog : DialogFragment() { private var _binding: TimeDialogBinding? = null private val binding get() = _binding!! private lateinit var etxtTime: EditText - private lateinit var timeSetup: LinearLayout - private lateinit var timeDisplay: LinearLayout - private lateinit var time: TextView private lateinit var chAutoEnable: CheckBox @UnstableApi override fun onStart() { @@ -74,17 +73,13 @@ class SleepTimerDialog : DialogFragment() { @UnstableApi override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { _binding = TimeDialogBinding.inflate(layoutInflater) val content = binding.root -// val content = View.inflate(context, R.layout.time_dialog, null) val builder = MaterialAlertDialogBuilder(requireContext()) builder.setTitle(R.string.sleep_timer_label) builder.setView(binding.root) builder.setPositiveButton(R.string.close_label, null) etxtTime = binding.etxtTime - timeSetup = binding.timeSetup - timeDisplay = binding.timeDisplay - timeDisplay.visibility = View.GONE - time = binding.time + binding.timeDisplay.visibility = View.GONE val extendSleepFiveMinutesButton = binding.extendSleepFiveMinutesButton extendSleepFiveMinutesButton.text = getString(R.string.extend_sleep_timer_label, 5) val extendSleepTenMinutesButton = binding.extendSleepTenMinutesButton @@ -101,27 +96,30 @@ class SleepTimerDialog : DialogFragment() { extendSleepTimer((20 * 1000 * 60).toLong()) } + binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + if (isChecked) etxtTime.visibility = View.GONE + else etxtTime.visibility = View.VISIBLE + } + etxtTime.setText(lastTimerValue()) etxtTime.postDelayed({ val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT) }, 100) - val cbShakeToReset = binding.cbShakeToReset - val cbVibrate = binding.cbVibrate chAutoEnable = binding.chAutoEnable val changeTimesButton = binding.changeTimesButton - cbShakeToReset.isChecked = shakeToReset() - cbVibrate.isChecked = vibrate() + binding.cbShakeToReset.isChecked = shakeToReset() + binding.cbVibrate.isChecked = vibrate() chAutoEnable.setChecked(autoEnable()) changeTimesButton.isEnabled = chAutoEnable.isChecked changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f - cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setShakeToReset(isChecked) } - cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) } + binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) } chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setAutoEnable(isChecked) changeTimesButton.isEnabled = isChecked @@ -135,23 +133,26 @@ class SleepTimerDialog : DialogFragment() { showTimeRangeDialog(from, to) } - val disableButton = binding.disableSleeptimerButton - disableButton.setOnClickListener { + binding.disableSleeptimerButton.setOnClickListener { playbackService?.taskManager?.disableSleepTimer() } - val setButton = binding.setSleeptimerButton - setButton.setOnClickListener { + + binding.setSleeptimerButton.setOnClickListener { if (!PlaybackService.isRunning) { Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show() return@setOnClickListener } try { - val time = etxtTime.getText().toString().toLong() + val time = if (binding.endEpisode.isChecked) { + val curPosition = curMedia?.getPosition() ?: 0 + val duration = curMedia?.getDuration() ?: 0 + val converter = TimeSpeedConverter(curSpeedMultiplier) + TimeUnit.MILLISECONDS.toMinutes(converter.convert(max((duration - curPosition).toDouble(), 0.0).toInt()).toLong()) // ms to minutes + } else etxtTime.getText().toString().toLong() + Logd(TAG, "Sleep timer set: $time") if (time == 0L) throw NumberFormatException("Timer must not be zero") - - setLastTimer(etxtTime.getText().toString()) + setLastTimer(time.toString()) setSleepTimer(timerMillis()) - closeKeyboard(content) } catch (e: NumberFormatException) { e.printStackTrace() @@ -167,12 +168,12 @@ class SleepTimerDialog : DialogFragment() { super.onDestroyView() } - fun extendSleepTimer(extendTime: Long) { + private fun extendSleepTimer(extendTime: Long) { val timeLeft = playbackService?.taskManager?.sleepTimerTimeLeft ?: Playable.INVALID_TIME.toLong() if (timeLeft != Playable.INVALID_TIME.toLong()) setSleepTimer(timeLeft + extendTime) } - fun setSleepTimer(time: Long) { + private fun setSleepTimer(time: Long) { playbackService?.taskManager?.setSleepTimer(time) } @@ -224,10 +225,10 @@ class SleepTimerDialog : DialogFragment() { } } - fun timerUpdated(event: FlowEvent.SleepTimerUpdatedEvent) { - timeDisplay.visibility = if (event.isOver || event.isCancelled) View.GONE else View.VISIBLE - timeSetup.visibility = if (event.isOver || event.isCancelled) View.VISIBLE else View.GONE - time.text = getDurationStringLong(event.getTimeLeft().toInt()) + private fun timerUpdated(event: FlowEvent.SleepTimerUpdatedEvent) { + binding.timeDisplay.visibility = if (event.isOver || event.isCancelled) View.GONE else View.VISIBLE + binding.timeSetup.visibility = if (event.isOver || event.isCancelled) View.VISIBLE else View.GONE + binding.time.text = getDurationStringLong(event.getTimeLeft().toInt()) } private fun closeKeyboard(content: View) { @@ -254,7 +255,7 @@ class SleepTimerDialog : DialogFragment() { private val paintSelected = Paint() private val paintText = Paint() private val bounds = RectF() - var touching: Int = 0 + private var touching: Int = 0 init { setup() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index 7ede9775..8411b68f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -261,7 +261,7 @@ import java.util.* Logd(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}") if (codeArray[2]) UserPreferences.setPlaybackSpeed(speed) if (codeArray[1]) { - val episode = (curMedia as? EpisodeMedia)?.episode ?: curEpisode + val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode if (episode != null) { var feed = episode.feed if (feed != null) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 965ea04e..9e169e4e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -23,6 +23,7 @@ import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode import ac.mdiq.podcini.receiver.MediaButtonReceiver +import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.model.Chapter import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.Playable @@ -229,7 +230,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } } currentMedia = curMedia - val item = (currentMedia as? EpisodeMedia)?.episode + val item = (currentMedia as? EpisodeMedia)?.episodeOrFetch() if (item != null) playerDetailsFragment?.setItem(item) updateUi() playerUI?.updateUi(currentMedia) @@ -327,8 +328,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { // Logd(TAG, "onPlayEvent ${event.episode.title}") - val media = event.media - if (currentMedia?.getIdentifier() == null || media?.getIdentifier() != currentMedia?.getIdentifier()) { + val media = event.media ?: return + if (currentMedia?.getIdentifier() == null || media.getIdentifier() != currentMedia?.getIdentifier()) { currentMedia = media playerUI?.updateUi(currentMedia) playerDetailsFragment?.setItem(curEpisode!!) @@ -430,7 +431,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar val isEpisodeMedia = currentMedia is EpisodeMedia toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia) - val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episode else null + val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episodeOrFetch() else null EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item) val mediaType = curMedia?.getMediaType() @@ -445,7 +446,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar override fun onMenuItemClick(menuItem: MenuItem): Boolean { val media: Playable = curMedia ?: return false - val feedItem = if (media is EpisodeMedia) media.episode else null + val feedItem = if (media is EpisodeMedia) media.episodeOrFetch() else null if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true val itemId = menuItem.itemId diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt index 86d6c9b5..bcf66e7a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt @@ -359,7 +359,7 @@ import kotlinx.coroutines.withContext } private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { - val item = (event.media as? EpisodeMedia)?.episode ?: return + val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return val pos = if (curIndex in 0.. = mutableListOf() private fun reconsile() { runOnIOScope { + val items = realm.query(Episode::class).query("media.episode == nil").find() + Logd(TAG, "number of episode with null backlink: ${items.size}") + for (item in items) { + upsert(item) { + it.media!!.episode = it + } + } nameEpisodeMap.clear() episodes.forEach { e -> var fileUrl = e.media?.fileUrl ?: return@forEach @@ -375,7 +384,7 @@ import java.util.* } private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { - val item = (event.media as? EpisodeMedia)?.episode ?: return + val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return val pos = if (curIndex in 0.. $0 AND lastPlayedTime <= $1", start, end).find() var episodes: MutableList = mutableListOf() for (m in medias) { - if (m.episode != null) episodes.add(m.episode!!) + val item_ = m.episodeOrFetch() + if (item_ != null) episodes.add(item_) } getPermutor(sortOrder).reorder(episodes) if (episodes.size > offset) episodes = episodes.subList(offset, min(episodes.size, offset+limit)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt index 80d7df59..733268de 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt @@ -459,9 +459,7 @@ import kotlin.concurrent.Volatile item.id = 0L item.feed = feed val media = item.media - if (media != null) { - media.episode = item - } + media?.episode = item } val fo = updateFeed(requireContext(), feed, false) Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index 6b58ac64..78043fda 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -148,7 +148,7 @@ class PlayerDetailsFragment : Fragment() { playable = curMedia if (playable != null && playable is EpisodeMedia) { val episodeMedia = playable as EpisodeMedia - currentItem = episodeMedia.episode + currentItem = episodeMedia.episodeOrFetch() showHomeText = false homeText = null } @@ -266,9 +266,9 @@ class PlayerDetailsFragment : Fragment() { when { playable?.getChapters() != null -> chapterControlVisible = playable!!.getChapters().isNotEmpty() playable is EpisodeMedia -> { - val fm: EpisodeMedia? = (playable as EpisodeMedia?) + val item_ = (playable as EpisodeMedia).episodeOrFetch() // If an item has chapters but they are not loaded yet, still display the button. - chapterControlVisible = fm?.episode != null && fm.episode!!.chapters.isNotEmpty() + chapterControlVisible = !item_?.chapters.isNullOrEmpty() } } val newVisibility = if (chapterControlVisible) View.VISIBLE else View.GONE diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index 8eb7f19d..6dcab5f1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -130,8 +130,10 @@ import java.util.* val queues = realm.query(PlayQueue::class).find() queueNames = queues.map { it.name }.toTypedArray() - val spinnerLayout = inflater.inflate(R.layout.queue_title_spinner, null) + val spinnerLayout = inflater.inflate(R.layout.queue_title_spinner, toolbar, false) queueSpinner = spinnerLayout.findViewById(R.id.queue_spinner) + val params = Toolbar.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + params.gravity = Gravity.CENTER_VERTICAL toolbar.addView(spinnerLayout) spinnerAdaptor = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, queueNames) spinnerAdaptor.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) @@ -148,7 +150,7 @@ import java.util.* (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) toolbar.inflateMenu(R.menu.queue) - refreshToolbarState() + refreshMenuItems() binding.progressBar.visibility = View.VISIBLE recyclerView = binding.recyclerView @@ -314,7 +316,7 @@ import java.util.* FlowEvent.QueueEvent.Action.MOVED, FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return } adapter?.updateDragDropEnabled() - refreshToolbarState() + refreshMenuItems() recyclerView.saveScrollPosition(TAG) refreshInfoBar() } @@ -407,7 +409,7 @@ import java.util.* } private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { - val item = (event.media as? EpisodeMedia)?.episode ?: return + val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return val pos = if (curIndex in 0..= 0) queueItems[pos].setPlayed(event.episode.isPlayed()) } - refreshToolbarState() + refreshMenuItems() } private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) { @@ -472,12 +474,21 @@ import java.util.* super.onDestroyView() } - private fun refreshToolbarState() { - val keepSorted: Boolean = isQueueKeepSorted - toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked) - toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted) - toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") - toolbar.menu?.findItem(R.id.add_queue)?.setVisible(queueNames.size<9) + private fun refreshMenuItems() { + if (showBin) { + toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(false) + toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(false) + toolbar.menu?.findItem(R.id.add_queue)?.setVisible(false) + toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(false) + toolbar.menu?.findItem(R.id.action_search)?.setVisible(false) + } else { + toolbar.menu?.findItem(R.id.action_search)?.setVisible(true) + toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(true) + toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked) + toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!isQueueKeepSorted) + toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") + toolbar.menu?.findItem(R.id.add_queue)?.setVisible(queueNames.size < 9) + } } @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { @@ -485,6 +496,7 @@ import java.util.* when (itemId) { R.id.show_bin -> { showBin = !showBin + refreshMenuItems() if (showBin) { item.setIcon(R.drawable.playlist_play) speedDialView.addActionItem(addToQueueActionItem) @@ -656,7 +668,7 @@ import java.util.* @UnstableApi private fun setQueueLocked(locked: Boolean) { isQueueLocked = locked - refreshToolbarState() + refreshMenuItems() adapter?.updateDragDropEnabled() if (queueItems.size == 0) { @@ -751,7 +763,7 @@ import java.util.* queueItems.clear() if (showBin) { queueItems.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.idsBinList) - .find().sortedBy { curQueue.idsBinList.indexOf(it.id) })) + .find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })) } else { curQueue.episodes.clear() curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) @@ -770,7 +782,7 @@ import java.util.* override fun onStartSelectMode() { swipeActions.detach() speedDialView.visibility = View.VISIBLE - refreshToolbarState() + refreshMenuItems() binding.infoBar.visibility = View.GONE } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index 96af0e01..53de00b5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -295,7 +295,7 @@ import java.lang.ref.WeakReference } private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { - val item = (event.media as? EpisodeMedia)?.episode ?: return + val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return val pos = if (curIndex in 0.. 0 || m.episode?.playState == Episode.PLAYED) + if (m.playbackCompletionTime > 0 || m.episodeOrFetch()?.playState == Episode.PLAYED) dur += m.duration else if (m.position > 0) dur += m.position } else dur += m.position diff --git a/app/src/main/res/layout/queue_title_spinner.xml b/app/src/main/res/layout/queue_title_spinner.xml index 55c0adec..89676cb1 100644 --- a/app/src/main/res/layout/queue_title_spinner.xml +++ b/app/src/main/res/layout/queue_title_spinner.xml @@ -1,15 +1,14 @@ + + + android:textColor="?android:attr/textColorPrimary" + tools:ignore="HardcodedText"/>