From a6f83b71c0379cc175a21d0e65141ec741a5ceb6 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Thu, 29 Feb 2024 13:53:21 +0100 Subject: [PATCH] enhancement on tags and player controls --- .../podcini/playback/PlaybackController.kt | 5 +- .../service/playback/ExoPlayerWrapper.kt | 6 +- .../podcini/service/playback/LocalPSMP.kt | 181 ++++++------- .../service/playback/PlaybackService.kt | 1 + .../java/ac/mdiq/podcini/storage/DBReader.kt | 15 +- .../java/ac/mdiq/podcini/storage/DBWriter.kt | 38 +-- .../podcini/storage/model/feed/FeedMedia.kt | 18 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 5 +- .../ui/activity/SelectSubscriptionActivity.kt | 8 +- .../adapter/SubscriptionsRecyclerAdapter.kt | 74 ++---- .../podcini/ui/dialog/TagSettingsDialog.kt | 60 ++--- .../podcini/ui/dialog/VariableSpeedDialog.kt | 4 +- .../ui/fragment/AudioPlayerFragment.kt | 48 ++-- .../ui/fragment/ExternalPlayerFragment.kt | 246 +++++++++++++++--- .../mdiq/podcini/ui/fragment/ItemFragment.kt | 10 +- .../podcini/ui/fragment/NavDrawerFragment.kt | 78 ++---- .../ui/fragment/SubscriptionFragment.kt | 9 +- .../podcini/ui/view/PlaybackSpeedSeekBar.kt | 22 +- .../view/viewholder/EpisodeItemViewHolder.kt | 4 +- .../java/ac/mdiq/podcini/util/ChapterUtils.kt | 6 +- .../main/res/layout/audioplayer_fragment.xml | 7 +- app/src/main/res/layout/edit_tags_dialog.xml | 10 +- .../res/layout/external_player_fragment.xml | 221 ++++++++++++---- .../main/res/layout/speed_select_dialog.xml | 27 ++ app/src/main/res/values/dimens.xml | 4 +- app/src/main/res/values/strings.xml | 3 + 26 files changed, 675 insertions(+), 435 deletions(-) diff --git a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt index e9b50177..f9b6f92c 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt @@ -331,8 +331,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { if (media is FeedMedia) { media.setPosition(time) DBWriter.setFeedItem(media.getItem()) - EventBus.getDefault().post(PlaybackPositionEvent(time, - media.getDuration())) + EventBus.getDefault().post(PlaybackPositionEvent(time, media.getDuration())) } } } @@ -358,7 +357,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { val audioTracks: List get() { - if (playbackService == null || playbackService!!.audioTracks.isEmpty()) { + if (playbackService?.audioTracks.isNullOrEmpty()) { return emptyList() } return playbackService!!.audioTracks.filterNotNull().map { it } diff --git a/app/src/main/java/ac/mdiq/podcini/service/playback/ExoPlayerWrapper.kt b/app/src/main/java/ac/mdiq/podcini/service/playback/ExoPlayerWrapper.kt index 72c2b49b..f8af723c 100644 --- a/app/src/main/java/ac/mdiq/podcini/service/playback/ExoPlayerWrapper.kt +++ b/app/src/main/java/ac/mdiq/podcini/service/playback/ExoPlayerWrapper.kt @@ -46,8 +46,8 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { private val bufferingUpdateDisposable: Disposable private lateinit var exoPlayer: ExoPlayer private lateinit var trackSelector: DefaultTrackSelector - private var loudnessEnhancer: LoudnessEnhancer? = null + private var loudnessEnhancer: LoudnessEnhancer? = null private var mediaSource: MediaSource? = null private var audioSeekCompleteListener: Runnable? = null private var audioCompletionListener: Runnable? = null @@ -61,7 +61,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { playbackParameters = exoPlayer.playbackParameters bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { tickNumber: Long? -> + .subscribe { bufferingUpdateListener?.accept(exoPlayer.bufferedPercentage) } } @@ -198,7 +198,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { // .setUserAgent(ClientConfig.USER_AGENT); val httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory) - .setUserAgent(ac.mdiq.podcini.util.config.ClientConfig.USER_AGENT) + .setUserAgent(ClientConfig.USER_AGENT) if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) { val requestProperties = HashMap() diff --git a/app/src/main/java/ac/mdiq/podcini/service/playback/LocalPSMP.kt b/app/src/main/java/ac/mdiq/podcini/service/playback/LocalPSMP.kt index 1670ae4c..b3f140d3 100644 --- a/app/src/main/java/ac/mdiq/podcini/service/playback/LocalPSMP.kt +++ b/app/src/main/java/ac/mdiq/podcini/service/playback/LocalPSMP.kt @@ -134,7 +134,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia } else { // stop playback of this episode if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { - mediaPlayer!!.stop() + mediaPlayer?.stop() } // set temporarily to pause in order to update list with current position if (playerStatus == PlayerStatus.PLAYING) { @@ -150,31 +150,32 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia } } - this.media = playable + media = playable this.stream = stream - this.mediaType = media!!.getMediaType() - this.videoSize = null + mediaType = media!!.getMediaType() + videoSize = null createMediaPlayer() this@LocalPSMP.startWhenPrepared.set(startWhenPrepared) setPlayerStatus(PlayerStatus.INITIALIZING, media) try { callback.ensureMediaInfoLoaded(media!!) callback.onMediaChanged(false) +// TODO: speed setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence) if (stream) { if (media!!.getStreamUrl() != null) { if (playable is FeedMedia && playable.getItem()?.feed?.preferences != null) { val preferences = playable.getItem()!!.feed!!.preferences!! - mediaPlayer!!.setDataSource( + mediaPlayer?.setDataSource( media!!.getStreamUrl()!!, preferences.username, preferences.password) } else { - mediaPlayer!!.setDataSource(media!!.getStreamUrl()!!) + mediaPlayer?.setDataSource(media!!.getStreamUrl()!!) } } } else if (media!!.getLocalMediaUrl() != null && File(media!!.getLocalMediaUrl()!!).canRead()) { - mediaPlayer!!.setDataSource(media!!.getLocalMediaUrl()!!) + mediaPlayer?.setDataSource(media!!.getLocalMediaUrl()!!) } else { throw IOException("Unable to read local file " + media!!.getLocalMediaUrl()) } @@ -185,7 +186,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (prepareImmediately) { setPlayerStatus(PlayerStatus.PREPARING, media) - mediaPlayer!!.prepare() + mediaPlayer?.prepare() onPrepared(startWhenPrepared) } } catch (e: IOException) { @@ -214,17 +215,17 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia Log.d(TAG, "Audiofocus successfully requested") Log.d(TAG, "Resuming/Starting playback") acquireWifiLockIfNecessary() - +// TODO: speed setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence) setVolume(1.0f, 1.0f) - if (playerStatus == PlayerStatus.PREPARED && media!!.getPosition() > 0) { + if (media != null && playerStatus == PlayerStatus.PREPARED && media!!.getPosition() > 0) { val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( media!!.getPosition(), media!!.getLastPlayedTime()) seekTo(newPosition) } - mediaPlayer!!.start() + mediaPlayer?.start() setPlayerStatus(PlayerStatus.PLAYING, media) pausedBecauseOfTransientAudiofocusLoss = false @@ -252,7 +253,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia releaseWifiLockIfNecessary() if (playerStatus == PlayerStatus.PLAYING) { Log.d(TAG, "Pausing playback.") - mediaPlayer!!.pause() + mediaPlayer?.pause() setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()) if (abandonFocus) { @@ -282,7 +283,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (playerStatus == PlayerStatus.INITIALIZED) { Log.d(TAG, "Preparing media player") setPlayerStatus(PlayerStatus.PREPARING, media) - mediaPlayer!!.prepare() + mediaPlayer?.prepare() onPrepared(startWhenPrepared.get()) } } @@ -294,18 +295,20 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia check(playerStatus == PlayerStatus.PREPARING) { "Player is not in PREPARING state" } Log.d(TAG, "Resource prepared") - if (mediaType == MediaType.VIDEO) { + if (mediaPlayer != null && mediaType == MediaType.VIDEO) { videoSize = Pair(mediaPlayer!!.videoWidth, mediaPlayer!!.videoHeight) } - // TODO this call has no effect! - if (media!!.getPosition() > 0) { - seekTo(media!!.getPosition()) - } + if (media != null) { + // TODO this call has no effect! + if (media!!.getPosition() > 0) { + seekTo(media!!.getPosition()) + } - if (media!!.getDuration() <= 0) { - Log.d(TAG, "Setting duration of media") - media!!.setDuration(mediaPlayer!!.duration) + if (media!!.getDuration() <= 0) { + Log.d(TAG, "Setting duration of media") + if (mediaPlayer != null) media!!.setDuration(mediaPlayer!!.duration) + } } setPlayerStatus(PlayerStatus.PREPARED, media) @@ -362,9 +365,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia seekLatch = CountDownLatch(1) statusBeforeSeeking = playerStatus setPlayerStatus(PlayerStatus.SEEKING, media, getPosition()) - mediaPlayer!!.seekTo(t) + mediaPlayer?.seekTo(t) if (statusBeforeSeeking == PlayerStatus.PREPARED) { - media!!.setPosition(t) + media?.setPosition(t) } try { seekLatch!!.await(3, TimeUnit.SECONDS) @@ -372,7 +375,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia Log.e(TAG, Log.getStackTraceString(e)) } } else if (playerStatus == PlayerStatus.INITIALIZED) { - media!!.setPosition(t) + media?.setPosition(t) startWhenPrepared.set(false) prepare() } @@ -398,7 +401,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia override fun getDuration(): Int { var retVal = Playable.INVALID_TIME if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - retVal = mediaPlayer!!.duration + if (mediaPlayer != null) retVal = mediaPlayer!!.duration } if (retVal <= 0 && media != null && media!!.getDuration() > 0) { retVal = media!!.getDuration() @@ -412,7 +415,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia override fun getPosition(): Int { var retVal = Playable.INVALID_TIME if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) { - retVal = mediaPlayer!!.currentPosition + if (mediaPlayer != null) retVal = mediaPlayer!!.currentPosition } if (retVal <= 0 && media != null && media!!.getPosition() >= 0) { retVal = media!!.getPosition() @@ -435,7 +438,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia override fun setPlaybackParams(speed: Float, skipSilence: Boolean) { Log.d(TAG, "Playback speed was set to $speed") EventBus.getDefault().post(SpeedChangedEvent(speed)) - mediaPlayer!!.setPlaybackParams(speed, skipSilence) + mediaPlayer?.setPlaybackParams(speed, skipSilence) } /** @@ -443,8 +446,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia */ override fun getPlaybackSpeed(): Float { var retVal = 1f - if ((playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.INITIALIZED || playerStatus == PlayerStatus.PREPARED)) { - retVal = mediaPlayer!!.currentSpeedMultiplier + if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.INITIALIZED || playerStatus == PlayerStatus.PREPARED) { + if (mediaPlayer != null) retVal = mediaPlayer!!.currentSpeedMultiplier } return retVal } @@ -466,7 +469,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia volumeRight *= adaptionFactor } } - mediaPlayer!!.setVolume(volumeLeft, volumeRight) + mediaPlayer?.setVolume(volumeLeft, volumeRight) Log.d(TAG, "Media player volume was set to $volumeLeft $volumeRight") } @@ -507,7 +510,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia override fun resetVideoSurface() { if (mediaType == MediaType.VIDEO) { Log.d(TAG, "Resetting video surface") - mediaPlayer!!.setDisplay(null) + mediaPlayer?.setDisplay(null) reinit() } else { Log.e(TAG, "Resetting video surface for media of Audio type") @@ -543,21 +546,20 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia } override fun getAudioTracks(): List { - return mediaPlayer!!.audioTracks + return mediaPlayer?.audioTracks?: listOf() } override fun setAudioTrack(track: Int) { - mediaPlayer!!.setAudioTrack(track) + if (mediaPlayer != null) mediaPlayer!!.setAudioTrack(track) } override fun getSelectedAudioTrack(): Int { - return mediaPlayer!!.selectedAudioTrack + return mediaPlayer?.selectedAudioTrack?:0 } private fun createMediaPlayer() { - if (mediaPlayer != null) { - mediaPlayer!!.release() - } + mediaPlayer?.release() + if (media == null) { mediaPlayer = null playerStatus = PlayerStatus.STOPPED @@ -573,50 +575,53 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (isShutDown) { return@OnAudioFocusChangeListener } - if (!PlaybackService.isRunning) { - abandonAudioFocus() - Log.d(TAG, "onAudioFocusChange: PlaybackService is no longer running") - return@OnAudioFocusChangeListener - } - if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - Log.d(TAG, "Lost audio focus") - pause(true, false) - callback.shouldStop() - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK - && !UserPreferences.shouldPauseForFocusLoss()) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Ducking...") - setVolume(0.25f, 0.25f) + when { + !PlaybackService.isRunning -> { + abandonAudioFocus() + Log.d(TAG, "onAudioFocusChange: PlaybackService is no longer running") + return@OnAudioFocusChangeListener + } + focusChange == AudioManager.AUDIOFOCUS_LOSS -> { + Log.d(TAG, "Lost audio focus") + pause(true, false) + callback.shouldStop() + } + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK + && !UserPreferences.shouldPauseForFocusLoss() -> { + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Lost audio focus temporarily. Ducking...") + setVolume(0.25f, 0.25f) + pausedBecauseOfTransientAudiofocusLoss = false + } + } + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + if (playerStatus == PlayerStatus.PLAYING) { + Log.d(TAG, "Lost audio focus temporarily. Pausing...") + mediaPlayer?.pause() // Pause without telling the PlaybackService + pausedBecauseOfTransientAudiofocusLoss = true + + audioFocusCanceller.removeCallbacksAndMessages(null) + audioFocusCanceller.postDelayed({ + if (pausedBecauseOfTransientAudiofocusLoss) { + // Still did not get back the audio focus. Now actually pause. + pause(true, false) + } + }, 30000) + } + } + focusChange == AudioManager.AUDIOFOCUS_GAIN -> { + Log.d(TAG, "Gained audio focus") + audioFocusCanceller.removeCallbacksAndMessages(null) + if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now + mediaPlayer?.start() + } else { // we ducked => raise audio level back + setVolume(1.0f, 1.0f) + } pausedBecauseOfTransientAudiofocusLoss = false } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT - || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Pausing...") - mediaPlayer!!.pause() // Pause without telling the PlaybackService - pausedBecauseOfTransientAudiofocusLoss = true - - audioFocusCanceller.removeCallbacksAndMessages(null) - audioFocusCanceller.postDelayed({ - if (pausedBecauseOfTransientAudiofocusLoss) { - // Still did not get back the audio focus. Now actually pause. - pause(true, false) - } - }, 30000) - } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - Log.d(TAG, "Gained audio focus") - audioFocusCanceller.removeCallbacksAndMessages(null) - if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now - mediaPlayer!!.start() - } else { // we ducked => raise audio level back - setVolume(1.0f, 1.0f) - } - pausedBecauseOfTransientAudiofocusLoss = false } } - init { mediaType = MediaType.UNKNOWN @@ -639,16 +644,12 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia val isPlaying = playerStatus == PlayerStatus.PLAYING // we're relying on the position stored in the Playable object for post-playback processing - if (media != null) { - val position = getPosition() - if (position >= 0) { - media!!.setPosition(position) - } + val position = getPosition() + if (position >= 0) { + media?.setPosition(position) } - if (mediaPlayer != null) { - mediaPlayer!!.reset() - } + mediaPlayer?.reset() abandonAudioFocus() @@ -726,21 +727,21 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia } private fun clearMediaPlayerListeners() { + if (mediaPlayer == null) return mediaPlayer!!.setOnCompletionListener {} mediaPlayer!!.setOnSeekCompleteListener {} - mediaPlayer!!.setOnBufferingUpdateListener { percent: Int? -> } + mediaPlayer!!.setOnBufferingUpdateListener { } mediaPlayer!!.setOnErrorListener { x: String? -> } } private fun genericSeekCompleteListener() { Log.d(TAG, "genericSeekCompleteListener") - if (seekLatch != null) { - seekLatch!!.countDown() - } + seekLatch?.countDown() + if (playerStatus == PlayerStatus.PLAYING) { - callback.onPlaybackStart(media!!, getPosition()) + if (media != null) callback.onPlaybackStart(media!!, getPosition()) } - if (playerStatus == PlayerStatus.SEEKING) { + if (playerStatus == PlayerStatus.SEEKING && statusBeforeSeeking != null) { setPlayerStatus(statusBeforeSeeking!!, media, getPosition()) } } diff --git a/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt b/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt index 960a4328..9e0bcf54 100644 --- a/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt +++ b/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt @@ -1517,6 +1517,7 @@ class PlaybackService : MediaBrowserServiceCompat() { @Subscribe(threadMode = ThreadMode.MAIN) @Suppress("unused") fun speedPresetChanged(event: SpeedPresetChangedEvent) { +// TODO: speed if (playable is FeedMedia) { if ((playable as FeedMedia).getItem()?.feed?.id == event.feedId) { if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) { diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt index ffe959a9..8bbaa4dd 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt @@ -18,6 +18,7 @@ import ac.mdiq.podcini.storage.database.mapper.DownloadResultCursorMapper.conver import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.feedCounterSetting import ac.mdiq.podcini.preferences.UserPreferences.feedOrder +import ac.mdiq.podcini.storage.model.feed.FeedPreferences.Companion.TAG_ROOT import java.util.* import kotlin.math.min @@ -60,7 +61,8 @@ object DBReader { fun updateFeedList(adapter: PodDBAdapter) { synchronized(feedListLock) { adapter.allFeedsCursor.use { cursor -> - feeds = ArrayList(cursor.count) +// feeds = ArrayList(cursor.count) + feeds.clear() while (cursor.moveToNext()) { val feed = extractFeedFromCursorRow(cursor) feeds.add(feed) @@ -70,14 +72,16 @@ object DBReader { } } - private fun buildTags() { + fun buildTags() { val tagsSet = mutableSetOf() for (feed in feeds) { for (tag in feed.preferences!!.getTags()) { - tagsSet.add(tag) + if (tag != TAG_ROOT) tagsSet.add(tag) } } - tags = tagsSet.toMutableList() + tags.clear() + tags.addAll(tagsSet) + tags.sort() } @JvmStatic @@ -841,7 +845,8 @@ object DBReader { val feedCounters: Map = adapter.getFeedCounters(feedCounterSetting) // getFeedList(adapter) - if (subscriptionsFilter != null) { +// TODO: + if (false || subscriptionsFilter != null) { feeds = subscriptionsFilter.filter(feeds, feedCounters as Map).toMutableList() } diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt index a15f70ff..3fff219b 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt @@ -108,7 +108,7 @@ import java.util.concurrent.TimeUnit // Local feed val documentFile = DocumentFile.fromSingleUri(context, Uri.parse(media.getFile_url())) if (documentFile == null || !documentFile.exists() || !documentFile.delete()) { - EventBus.getDefault().post(ac.mdiq.podcini.util.event.MessageEvent(context.getString(R.string.delete_local_failed))) + EventBus.getDefault().post(MessageEvent(context.getString(R.string.delete_local_failed))) return false } media.setFile_url(null) @@ -117,7 +117,7 @@ import java.util.concurrent.TimeUnit // delete downloaded media file val mediaFile = File(media.getFile_url()!!) if (mediaFile.exists() && !mediaFile.delete()) { - val evt = ac.mdiq.podcini.util.event.MessageEvent(context.getString(R.string.delete_failed)) + val evt = MessageEvent(context.getString(R.string.delete_failed)) EventBus.getDefault().post(evt) return false } @@ -180,7 +180,7 @@ import java.util.concurrent.TimeUnit if (!feed.isLocalFeed && feed.download_url != null) { SynchronizationQueueSink.enqueueFeedRemovedIfSynchronizationIsActive(context, feed.download_url!!) } - EventBus.getDefault().post(ac.mdiq.podcini.util.event.FeedListUpdateEvent(feed)) + EventBus.getDefault().post(FeedListUpdateEvent(feed)) } } @@ -233,7 +233,7 @@ import java.util.concurrent.TimeUnit // we assume we also removed download log entries for the feed or its media files. // especially important if download or refresh failed, as the user should not be able // to retry these - EventBus.getDefault().post(ac.mdiq.podcini.util.event.DownloadLogEvent.listUpdated()) + EventBus.getDefault().post(DownloadLogEvent.listUpdated()) val backupManager = BackupManager(context) backupManager.dataChanged() @@ -261,7 +261,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.clearDownloadLog() adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.DownloadLogEvent.listUpdated()) + EventBus.getDefault().post(DownloadLogEvent.listUpdated()) } } @@ -309,7 +309,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setDownloadStatus(status!!) adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.DownloadLogEvent.listUpdated()) + EventBus.getDefault().post(DownloadLogEvent.listUpdated()) } } @@ -406,11 +406,11 @@ import java.util.concurrent.TimeUnit var queueModified = false val markAsUnplayedIds = LongList() - val events: MutableList = ArrayList() + val events: MutableList = ArrayList() val updatedItems: MutableList = ArrayList() val positionCalculator = ItemEnqueuePositionCalculator(enqueueLocation) - val currentlyPlaying = createInstanceFromPreferences(context!!) + val currentlyPlaying = createInstanceFromPreferences(context) var insertPosition = positionCalculator.calcPosition(queue, currentlyPlaying) for (itemId in itemIds) { if (!itemListContains(queue, itemId)) { @@ -454,7 +454,7 @@ import java.util.concurrent.TimeUnit * @param queue The queue to be sorted. * @param events Replaces the events by a single SORT event if the list has to be sorted automatically. */ - private fun applySortOrder(queue: MutableList, events: MutableList) { + private fun applySortOrder(queue: MutableList, events: MutableList) { if (!isQueueKeepSorted) { // queue is not in keep sorted mode, there's nothing to do return @@ -471,7 +471,7 @@ import java.util.concurrent.TimeUnit // Replace ADDED events by a single SORTED event events.clear() - events.add(ac.mdiq.podcini.util.event.QueueEvent.sorted(queue)) + events.add(QueueEvent.sorted(queue)) } /** @@ -521,7 +521,7 @@ import java.util.concurrent.TimeUnit val queue = getQueue(adapter).toMutableList() var queueModified = false - val events: MutableList = ArrayList() + val events: MutableList = ArrayList() val updatedItems: MutableList = ArrayList() for (itemId in itemIds) { val position = indexInItemList(queue, itemId) @@ -712,7 +712,7 @@ import java.util.concurrent.TimeUnit adapter.setFeedItemRead(played, *itemIds) adapter.close() if (broadcastUpdate) { - EventBus.getDefault().post(ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent()) + EventBus.getDefault().post(UnreadItemsUpdateEvent()) } } } @@ -741,7 +741,7 @@ import java.util.concurrent.TimeUnit adapter.setFeedItemRead(played, itemId, mediaId, resetMediaPosition) adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent()) + EventBus.getDefault().post(UnreadItemsUpdateEvent()) } } @@ -756,7 +756,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED, feedId) adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent()) + EventBus.getDefault().post(UnreadItemsUpdateEvent()) } } @@ -770,7 +770,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED) adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent()) + EventBus.getDefault().post(UnreadItemsUpdateEvent()) } } @@ -882,7 +882,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedPreferences(preferences) adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.FeedListUpdateEvent(preferences.feedID)) + EventBus.getDefault().post(FeedListUpdateEvent(preferences.feedID)) } } @@ -913,7 +913,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed) adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.FeedListUpdateEvent(feedId)) + EventBus.getDefault().post(FeedListUpdateEvent(feedId)) } } @@ -923,7 +923,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedCustomTitle(feed.id, feed.getCustomTitle()) adapter.close() - EventBus.getDefault().post(ac.mdiq.podcini.util.event.FeedListUpdateEvent(feed)) + EventBus.getDefault().post(FeedListUpdateEvent(feed)) } } @@ -948,7 +948,7 @@ import java.util.concurrent.TimeUnit permutor.reorder(queue) adapter.setQueue(queue) if (broadcastUpdate) { - EventBus.getDefault().post(ac.mdiq.podcini.util.event.QueueEvent.sorted(queue)) + EventBus.getDefault().post(QueueEvent.sorted(queue)) } adapter.close() } diff --git a/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt b/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt index 7c933866..82cf02aa 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/model/feed/FeedMedia.kt @@ -40,8 +40,7 @@ class FeedMedia : FeedFile, Playable { var itemId: Long = 0 private set - constructor(i: FeedItem?, download_url: String?, size: Long, - mime_type: String? + constructor(i: FeedItem?, download_url: String?, size: Long, mime_type: String? ) : super(null, download_url, false) { this.item = i this.size = size @@ -268,10 +267,7 @@ class FeedMedia : FeedFile, Playable { } override fun getChapters(): List { - if (item?.chapters == null) { - return listOf() - } - return item!!.chapters!! + return item?.chapters?:listOf() } override fun chaptersLoaded(): Boolean { @@ -279,17 +275,11 @@ class FeedMedia : FeedFile, Playable { } override fun getWebsiteLink(): String? { - if (item == null) { - return null - } - return item!!.link + return item?.link } override fun getFeedTitle(): String { - if (item?.feed?.title == null) { - return "" - } - return item!!.feed!!.title!! + return item?.feed?.title?:"" } override fun getIdentifier(): Any { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt index b72068ff..e00f0172 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -547,9 +547,8 @@ class MainActivity : CastEnabledActivity() { intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG) -> { val tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG) val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS) - if (tag != null) { - loadFragment(tag, args) - } + if (tag != null) loadFragment(tag, args) + bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) } intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false) -> { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt index 47dde19d..983b4e90 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt @@ -68,17 +68,13 @@ class SelectSubscriptionActivity : AppCompatActivity() { } } - fun getFeedItems(items: List, result: MutableList): List { + fun getFeedItems(items: List, result: MutableList): List { for (item in items) { if (item == null) continue -// if (item.type == NavDrawerData.DrawerItem.Type.TAG) { -// getFeedItems((item as NavDrawerData.TagDrawerItem).children, result) -// } else { - val feed: Feed = (item as NavDrawerData.FeedDrawerItem).feed + val feed: Feed = item.feed if (!result.contains(feed)) { result.add(feed) } -// } } return result } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/adapter/SubscriptionsRecyclerAdapter.kt b/app/src/main/java/ac/mdiq/podcini/ui/adapter/SubscriptionsRecyclerAdapter.kt index 19b3aef2..b0a06578 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/adapter/SubscriptionsRecyclerAdapter.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/adapter/SubscriptionsRecyclerAdapter.kt @@ -6,7 +6,6 @@ import ac.mdiq.podcini.storage.NavDrawerData import ac.mdiq.podcini.storage.model.feed.Feed import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.fragment.FeedItemlistFragment -import ac.mdiq.podcini.ui.fragment.SubscriptionFragment import android.content.Context import android.graphics.Rect import android.graphics.drawable.Drawable @@ -30,8 +29,8 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : View.OnCreateContextMenuListener { private val mainActivityRef: WeakReference = WeakReference(mainActivity) - private var listItems: List - private var selectedItem: NavDrawerData.DrawerItem? = null + private var listItems: List + private var selectedItem: NavDrawerData.FeedDrawerItem? = null private var longPressedPosition: Int = 0 // used to init actionMode init { @@ -43,7 +42,7 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : return listItems[position] } - fun getSelectedItem(): NavDrawerData.DrawerItem? { + fun getSelectedItem(): NavDrawerData.FeedDrawerItem? { return selectedItem } @@ -53,15 +52,13 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : } @UnstableApi override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) { - val drawerItem: NavDrawerData.DrawerItem = listItems[position] - val isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED + val drawerItem: NavDrawerData.FeedDrawerItem = listItems[position] holder.bind(drawerItem) holder.itemView.setOnCreateContextMenuListener(this) if (inActionMode()) { - if (isFeed) { - holder.selectCheckbox.visibility = View.VISIBLE - holder.selectView.visibility = View.VISIBLE - } + holder.selectCheckbox.visibility = View.VISIBLE + holder.selectView.visibility = View.VISIBLE + holder.selectCheckbox.setChecked((isSelected(position))) holder.selectCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setSelected(holder.bindingAdapterPosition, @@ -76,9 +73,7 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : holder.itemView.setOnLongClickListener { if (!inActionMode()) { - if (isFeed) { - longPressedPosition = holder.bindingAdapterPosition - } + longPressedPosition = holder.bindingAdapterPosition selectedItem = drawerItem } false @@ -89,9 +84,7 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : if (e.isFromSource(InputDevice.SOURCE_MOUSE) && e.buttonState == MotionEvent.BUTTON_SECONDARY) { if (!inActionMode()) { - if (isFeed) { - longPressedPosition = holder.bindingAdapterPosition - } + longPressedPosition = holder.bindingAdapterPosition selectedItem = drawerItem } } @@ -99,16 +92,11 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : false } holder.itemView.setOnClickListener { - if (isFeed) { - if (inActionMode()) { - holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition)) - } else { - val fragment: Fragment = FeedItemlistFragment - .newInstance((drawerItem as NavDrawerData.FeedDrawerItem).feed.id) - mainActivityRef.get()?.loadChildFragment(fragment) - } - } else if (!inActionMode()) { - val fragment: Fragment = SubscriptionFragment.newInstance(drawerItem.title) + if (inActionMode()) { + holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition)) + } else { + val fragment: Fragment = FeedItemlistFragment + .newInstance(drawerItem.feed.id) mainActivityRef.get()?.loadChildFragment(fragment) } } @@ -130,12 +118,8 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : return } val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater - if (selectedItem?.type == NavDrawerData.DrawerItem.Type.FEED) { - inflater.inflate(R.menu.nav_feed_context, menu) - menu.findItem(R.id.multi_select).setVisible(true) - } else { - inflater.inflate(R.menu.nav_folder_context, menu) - } + inflater.inflate(R.menu.nav_feed_context, menu) + menu.findItem(R.id.multi_select).setVisible(true) menu.setHeaderTitle(selectedItem?.title) } @@ -152,17 +136,15 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : val items = ArrayList() for (i in 0 until itemCount) { if (isSelected(i)) { - val drawerItem: NavDrawerData.DrawerItem = listItems[i] - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - val feed: Feed = (drawerItem as NavDrawerData.FeedDrawerItem).feed - items.add(feed) - } + val drawerItem: NavDrawerData.FeedDrawerItem = listItems[i] + val feed: Feed = drawerItem.feed + items.add(feed) } } return items } - fun setItems(listItems: List) { + fun setItems(listItems: List) { this.listItems = listItems notifyDataSetChanged() } @@ -180,28 +162,24 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) : private val errorIcon: View = binding.errorIcon - fun bind(drawerItem: NavDrawerData.DrawerItem) { + fun bind(drawerItem: NavDrawerData.FeedDrawerItem) { val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context, R.drawable.ic_checkbox_background) selectView.background = drawable // Setting this in XML crashes API <= 21 title.text = drawerItem.title producer.text = drawerItem.producer coverImage.contentDescription = drawerItem.title if (drawerItem.counter > 0) { - count.text = NumberFormat.getInstance().format(drawerItem.counter.toLong()) + " episodes" + count.text = NumberFormat.getInstance().format(drawerItem.feed.items.size.toLong()) + " episodes" count.visibility = View.VISIBLE } else { count.visibility = View.GONE } val coverLoader = CoverLoader(mainActivityRef.get()!!) - if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) { - val feed: Feed = (drawerItem as NavDrawerData.FeedDrawerItem).feed - coverLoader.withUri(feed.imageUrl) - errorIcon.visibility = if (feed.hasLastUpdateFailed()) View.VISIBLE else View.GONE - } else { - coverLoader.withResource(R.drawable.ic_tag) - errorIcon.visibility = View.GONE - } + val feed: Feed = drawerItem.feed + coverLoader.withUri(feed.imageUrl) + errorIcon.visibility = if (feed.hasLastUpdateFailed()) View.VISIBLE else View.GONE + coverLoader.withCoverView(coverImage) coverLoader.load() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt index 01a00185..b288c20d 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt @@ -1,27 +1,27 @@ package ac.mdiq.podcini.ui.dialog +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.EditTagsDialogBinding +import ac.mdiq.podcini.storage.DBReader +import ac.mdiq.podcini.storage.DBWriter +import ac.mdiq.podcini.storage.model.feed.FeedPreferences +import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter +import ac.mdiq.podcini.ui.view.ItemOffsetDecoration import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import android.util.Log import android.view.View import android.widget.ArrayAdapter +import androidx.annotation.OptIn import androidx.fragment.app.DialogFragment +import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter -import ac.mdiq.podcini.storage.DBReader -import ac.mdiq.podcini.storage.DBWriter -import ac.mdiq.podcini.storage.NavDrawerData.DrawerItem -import ac.mdiq.podcini.databinding.EditTagsDialogBinding -import ac.mdiq.podcini.storage.model.feed.FeedPreferences -import ac.mdiq.podcini.ui.view.ItemOffsetDecoration -import androidx.annotation.OptIn -import androidx.media3.common.util.UnstableApi import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import java.io.Serializable class TagSettingsDialog : DialogFragment() { private lateinit var displayedTags: MutableList @@ -29,10 +29,10 @@ class TagSettingsDialog : DialogFragment() { private lateinit var adapter: SimpleChipAdapter override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val feedPreferencesList = - requireArguments().getSerializable(ARG_FEED_PREFERENCES) as? ArrayList - val commonTags: MutableSet = HashSet( - feedPreferencesList!![0].getTags()) + val serializedData: Serializable? = requireArguments().getSerializable(ARG_FEED_PREFERENCES) + val feedPreferencesList = if (serializedData is ArrayList<*>) serializedData.filterIsInstance() else listOf() +// val feedPreferencesList = serializedData as? List ?: listOf() + val commonTags: MutableSet = if (feedPreferencesList.isEmpty()) mutableSetOf() else HashSet(feedPreferencesList[0].getTags()) for (preference in feedPreferencesList) { commonTags.retainAll(preference.getTags()) @@ -54,11 +54,10 @@ class TagSettingsDialog : DialogFragment() { } } viewBinding.tagsRecycler.adapter = adapter - viewBinding.rootFolderCheckbox.isChecked = commonTags.contains(FeedPreferences.TAG_ROOT) +// viewBinding.rootFolderCheckbox.isChecked = commonTags.contains(FeedPreferences.TAG_ROOT) viewBinding.newTagTextInput.setEndIconOnClickListener { - addTag( - viewBinding.newTagEditText.text.toString().trim { it <= ' ' }) + addTag(viewBinding.newTagEditText.text.toString().trim { it <= ' ' }) } loadTags() @@ -79,30 +78,21 @@ class TagSettingsDialog : DialogFragment() { dialog.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> addTag(viewBinding.newTagEditText.text.toString().trim { it <= ' ' }) updatePreferencesTags(feedPreferencesList, commonTags) + DBReader.buildTags() } dialog.setNegativeButton(R.string.cancel_label, null) return dialog.create() } private fun loadTags() { - Observable.fromCallable> { -// val data = DBReader.getNavDrawerData(null) -// val items = data.items - val folders: MutableList = ArrayList() -// for (item in items) { -// if (item.type == DrawerItem.Type.TAG) { -// if (item.title != null) folders.add(item.title!!) -// } -// } - folders + Observable.fromCallable { + DBReader.getTags() } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { result: List -> - val acAdapter = ArrayAdapter( - requireContext(), - R.layout.single_tag_text_view, result) + val acAdapter = ArrayAdapter(requireContext(), R.layout.single_tag_text_view, result) viewBinding.newTagEditText.setAdapter(acAdapter) }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) @@ -118,11 +108,11 @@ class TagSettingsDialog : DialogFragment() { adapter.notifyDataSetChanged() } - @OptIn(UnstableApi::class) private fun updatePreferencesTags(feedPreferencesList: List?, commonTags: Set) { - if (viewBinding.rootFolderCheckbox.isChecked) { - displayedTags.add(FeedPreferences.TAG_ROOT) - } - for (preferences in feedPreferencesList!!) { + @OptIn(UnstableApi::class) private fun updatePreferencesTags(feedPreferencesList: List, commonTags: Set) { +// if (viewBinding.rootFolderCheckbox.isChecked) { +// displayedTags.add(FeedPreferences.TAG_ROOT) +// } + for (preferences in feedPreferencesList) { preferences.getTags().removeAll(commonTags) preferences.getTags().addAll(displayedTags) DBWriter.setFeedPreferences(preferences) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index dc55c98b..f7e1ca29 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -72,8 +72,8 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { val binding = SpeedSelectDialogBinding.inflate(inflater) // val root = View.inflate(context, R.layout.speed_select_dialog, null) speedSeekBar = binding.speedSeekBar - speedSeekBar.setProgressChangedListener { multiplier: Float? -> - controller?.setPlaybackSpeed(multiplier!!) + speedSeekBar.setProgressChangedListener { multiplier: Float -> + controller?.setPlaybackSpeed(multiplier) } val selectedSpeedsGrid = binding.selectedSpeedsGrid selectedSpeedsGrid.layoutManager = GridLayoutManager(context, 3) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 3a40c5cf..2c59e50f 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -44,6 +44,9 @@ import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView import ac.mdiq.podcini.ui.view.ChapterSeekBar import ac.mdiq.podcini.ui.view.PlayButton +import ac.mdiq.podcini.util.Converter +import ac.mdiq.podcini.util.event.PlayerErrorEvent +import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent import io.reactivex.Maybe import io.reactivex.MaybeEmitter import io.reactivex.android.schedulers.AndroidSchedulers @@ -105,7 +108,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar toolbar = viewBinding.toolbar toolbar.title = "" toolbar.setNavigationOnClickListener { - (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) + (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) } toolbar.setOnMenuItemClickListener(this) @@ -136,8 +139,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar setupLengthTextView() setupControlButtons() butPlaybackSpeed.setOnClickListener { - VariableSpeedDialog().show( - childFragmentManager, null) + VariableSpeedDialog().show(childFragmentManager, null) } sbPosition.setOnSeekBarChangeListener(this) @@ -204,10 +206,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar true } butPlay.setOnClickListener { - if (controller != null) { - controller!!.init() - controller!!.playPause() - } + controller?.init() + controller?.playPause() } butFF.setOnClickListener { if (controller != null) { @@ -227,18 +227,17 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsUpdate(event: ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent?) { + fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) { if (controller == null) { return } - updatePosition(PlaybackPositionEvent(controller!!.position, - controller!!.duration)) + updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration)) } @Subscribe(threadMode = ThreadMode.MAIN) fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { - (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED } } @@ -264,7 +263,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar private fun loadMediaInfo(includingChapters: Boolean) { disposable?.dispose() - disposable = Maybe.create { emitter: MaybeEmitter -> val media: Playable? = controller?.getMedia() if (media != null) { @@ -298,7 +296,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } override fun onPlaybackEnd() { - (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED } } } @@ -308,10 +306,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar if (media == null) { return } - updatePosition(PlaybackPositionEvent(media.getPosition(), - media.getDuration())) - updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed( - media))) + updatePosition(PlaybackPositionEvent(media.getPosition(), media.getDuration())) + updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) setChapterDividers(media) setupOptionsMenu(media) } @@ -370,18 +366,18 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar Log.w(TAG, "Could not react to position observer update because of invalid time") return } - txtvPosition.text = ac.mdiq.podcini.util.Converter.getDurationStringLong(currentPosition) + txtvPosition.text = Converter.getDurationStringLong(currentPosition) txtvPosition.setContentDescription(getString(R.string.position, - ac.mdiq.podcini.util.Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) + Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) showTimeLeft = UserPreferences.shouldShowRemainingTime() if (showTimeLeft) { txtvLength.setContentDescription(getString(R.string.remaining_time, - ac.mdiq.podcini.util.Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) - txtvLength.text = (if ((remainingTime > 0)) "-" else "") + ac.mdiq.podcini.util.Converter.getDurationStringLong(remainingTime) + Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) + txtvLength.text = (if ((remainingTime > 0)) "-" else "") + Converter.getDurationStringLong(remainingTime) } else { txtvLength.setContentDescription(getString(R.string.chapter_duration, - ac.mdiq.podcini.util.Converter.getDurationStringLocalized(requireContext(), duration.toLong()))) - txtvLength.text = ac.mdiq.podcini.util.Converter.getDurationStringLong(duration) + Converter.getDurationStringLocalized(requireContext(), duration.toLong()))) + txtvLength.text = Converter.getDurationStringLong(duration) } if (!sbPosition.isPressed) { @@ -396,7 +392,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } @Subscribe(threadMode = ThreadMode.MAIN) - fun mediaPlayerError(event: ac.mdiq.podcini.util.event.PlayerErrorEvent) { + fun mediaPlayerError(event: PlayerErrorEvent) { MediaPlayerErrorDialog.show(activity as Activity, event) } @@ -419,9 +415,9 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar sbPosition.highlightCurrentChapter() } txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: ("" - + "\n" + ac.mdiq.podcini.util.Converter.getDurationStringLong(position)) + + "\n" + Converter.getDurationStringLong(position)) } else { - txtvSeek.text = ac.mdiq.podcini.util.Converter.getDurationStringLong(position) + txtvSeek.text = Converter.getDurationStringLong(position) } } else if (duration != controller!!.duration) { updateUi(controller!!.getMedia()) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt index bc99028c..35daaffe 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt @@ -1,32 +1,44 @@ package ac.mdiq.podcini.ui.fragment +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.ExternalPlayerFragmentBinding +import ac.mdiq.podcini.feed.util.ImageResourceUtils.getEpisodeListImageLocation +import ac.mdiq.podcini.feed.util.ImageResourceUtils.getFallbackImageLocation +import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils +import ac.mdiq.podcini.playback.PlaybackController +import ac.mdiq.podcini.playback.base.PlayerStatus +import ac.mdiq.podcini.playback.event.PlaybackPositionEvent +import ac.mdiq.podcini.playback.event.PlaybackServiceEvent +import ac.mdiq.podcini.playback.event.SpeedChangedEvent +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.receiver.MediaButtonReceiver +import ac.mdiq.podcini.service.playback.PlaybackService.Companion.getPlayerActivityIntent +import ac.mdiq.podcini.storage.model.playback.MediaType +import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView +import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog +import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog +import ac.mdiq.podcini.ui.view.ChapterSeekBar +import ac.mdiq.podcini.ui.view.PlayButton +import ac.mdiq.podcini.util.Converter +import ac.mdiq.podcini.util.TimeSpeedConverter import android.os.Bundle import android.util.Log +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageButton import android.widget.ImageView -import android.widget.ProgressBar +import android.widget.SeekBar import android.widget.TextView +import androidx.annotation.OptIn import androidx.fragment.app.Fragment import androidx.media3.common.util.UnstableApi import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.android.material.bottomsheet.BottomSheetBehavior -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.ExternalPlayerFragmentBinding -import ac.mdiq.podcini.feed.util.ImageResourceUtils.getEpisodeListImageLocation -import ac.mdiq.podcini.feed.util.ImageResourceUtils.getFallbackImageLocation -import ac.mdiq.podcini.service.playback.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.playback.PlaybackController -import ac.mdiq.podcini.playback.event.PlaybackPositionEvent -import ac.mdiq.podcini.playback.event.PlaybackServiceEvent -import ac.mdiq.podcini.storage.model.playback.MediaType -import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlayerStatus -import ac.mdiq.podcini.ui.view.PlayButton -import androidx.annotation.OptIn import io.reactivex.Maybe import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable @@ -34,32 +46,60 @@ import io.reactivex.schedulers.Schedulers import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import java.text.DecimalFormat +import java.text.NumberFormat +import kotlin.math.max /** * Fragment which is supposed to be displayed outside of the MediaplayerActivity. */ -class ExternalPlayerFragment : Fragment() { +class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener { private lateinit var imgvCover: ImageView - private lateinit var txtvTitle: TextView private lateinit var butPlay: PlayButton - private lateinit var feedName: TextView - private lateinit var progressBar: ProgressBar + + lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView + lateinit var txtvPlaybackSpeed: TextView + + private lateinit var butRev: ImageButton + private lateinit var txtvRev: TextView + private lateinit var butFF: ImageButton + private lateinit var txtvFF: TextView + private lateinit var butSkip: ImageButton + + private lateinit var txtvPosition: TextView + private lateinit var txtvLength: TextView + private lateinit var sbPosition: ChapterSeekBar + + private var showTimeLeft = false private var controller: PlaybackController? = null private var disposable: Disposable? = null @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + savedInstanceState: Bundle?): View { val viewBinding = ExternalPlayerFragmentBinding.inflate(inflater) - Log.d(TAG, "fragment onCreateView") + + butPlaybackSpeed = viewBinding.butPlaybackSpeed + txtvPlaybackSpeed = viewBinding.txtvPlaybackSpeed imgvCover = viewBinding.imgvCover - txtvTitle = viewBinding.txtvTitle butPlay = viewBinding.butPlay - feedName = viewBinding.txtvAuthor - progressBar = viewBinding.episodeProgress + butRev = viewBinding.butRev + txtvRev = viewBinding.txtvRev + butFF = viewBinding.butFF + txtvFF = viewBinding.txtvFF + butSkip = viewBinding.butSkip + sbPosition = viewBinding.sbPosition + txtvPosition = viewBinding.txtvPosition + txtvLength = viewBinding.txtvLength + + setupLengthTextView() + setupControlButtons() + butPlaybackSpeed.setOnClickListener { + VariableSpeedDialog().show(childFragmentManager, null) + } + sbPosition.setOnSeekBarChangeListener(this) viewBinding.externalPlayerFragment.setOnClickListener { Log.d(TAG, "externalPlayerFragment was clicked") @@ -73,6 +113,7 @@ class ExternalPlayerFragment : Fragment() { } } } + controller = setupPlaybackController() controller!!.init() loadMediaInfo() @@ -86,6 +127,7 @@ class ExternalPlayerFragment : Fragment() { controller = null EventBus.getDefault().unregister(this) } + @UnstableApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -104,6 +146,39 @@ class ExternalPlayerFragment : Fragment() { loadMediaInfo() } + @OptIn(UnstableApi::class) private fun setupControlButtons() { + butRev.setOnClickListener { + if (controller != null) { + val curr: Int = controller!!.position + controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000) + } + } + butRev.setOnLongClickListener { + SkipPreferenceDialog.showSkipPreference(requireContext(), + SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev) + true + } + butPlay.setOnClickListener { + controller?.init() + controller?.playPause() + } + butFF.setOnClickListener { + if (controller != null) { + val curr: Int = controller!!.position + controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000) + } + } + butFF.setOnLongClickListener { + SkipPreferenceDialog.showSkipPreference(requireContext(), + SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) + false + } + butSkip.setOnClickListener { + activity?.sendBroadcast( + MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) + } + } + @UnstableApi private fun setupPlaybackController(): PlaybackController { return object : PlaybackController(requireActivity()) { @@ -121,15 +196,58 @@ class ExternalPlayerFragment : Fragment() { } } + @OptIn(UnstableApi::class) private fun setupLengthTextView() { + showTimeLeft = UserPreferences.shouldShowRemainingTime() + txtvLength.setOnClickListener(View.OnClickListener { + if (controller == null) { + return@OnClickListener + } + showTimeLeft = !showTimeLeft + UserPreferences.setShowRemainTimeSetting(showTimeLeft) + onPositionObserverUpdate(PlaybackPositionEvent(controller!!.position, controller!!.duration)) + }) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun updatePlaybackSpeedButton(event: SpeedChangedEvent) { + val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble()) + txtvPlaybackSpeed.text = speedStr + butPlaybackSpeed.setSpeed(event.newSpeed) + } + @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onPositionObserverUpdate(event: PlaybackPositionEvent?) { - if (controller == null) { - return - } else if (controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) { + fun onPositionObserverUpdate(event: PlaybackPositionEvent) { + if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) { return } - progressBar.progress = (controller!!.position.toDouble() / controller!!.duration * 100).toInt() + val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) + val currentPosition: Int = converter.convert(event.position) + val duration: Int = converter.convert(event.duration) + val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt()) + if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) { + Log.w(AudioPlayerFragment.TAG, "Could not react to position observer update because of invalid time") + return + } + + txtvPosition.text = Converter.getDurationStringLong(currentPosition) + txtvPosition.setContentDescription(getString(R.string.position, + Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) + val showTimeLeft = UserPreferences.shouldShowRemainingTime() + if (showTimeLeft) { + txtvLength.setContentDescription(getString(R.string.remaining_time, + Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) + txtvLength.text = (if ((remainingTime > 0)) "-" else "") + Converter.getDurationStringLong(remainingTime) + } else { + txtvLength.setContentDescription(getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(requireContext(), duration.toLong()))) + txtvLength.text = Converter.getDurationStringLong(duration) + } + + if (!sbPosition.isPressed) { + val progress: Float = (event.position.toFloat()) / event.duration + sbPosition.progress = (progress * sbPosition.max).toInt() + } } @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) @@ -145,6 +263,15 @@ class ExternalPlayerFragment : Fragment() { disposable?.dispose() } + @OptIn(UnstableApi::class) override fun onStart() { + super.onStart() + txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) + txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) + val media = controller?.getMedia() + if (media != null) updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) + + } + @UnstableApi override fun onPause() { super.onPause() @@ -160,7 +287,9 @@ class ExternalPlayerFragment : Fragment() { } disposable?.dispose() - disposable = Maybe.fromCallable { controller?.getMedia() } + disposable = Maybe.fromCallable { + controller?.getMedia() + } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ media: Playable? -> this.updateUi(media) }, @@ -168,14 +297,67 @@ class ExternalPlayerFragment : Fragment() { { (activity as MainActivity).setPlayerVisible(false) }) } + @OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (controller == null) return + +// if (fromUser) { +// val prog: Float = progress / (seekBar.max.toFloat()) +// val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) +// var position: Int = converter.convert((prog * controller!!.duration).toInt()) +// val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), position) +// if (newChapterIndex > -1) { +// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) { +// currentChapterIndex = newChapterIndex +// val media = controller!!.getMedia() +// position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0 +// seekedToChapterStart = true +// controller!!.seekTo(position) +// updateUi(controller!!.getMedia()) +// sbPosition.highlightCurrentChapter() +// } +//// txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: ("" +//// + "\n" + Converter.getDurationStringLong(position)) +// } else { +//// txtvSeek.text = Converter.getDurationStringLong(position) +// } +// } else if (duration != controller!!.duration) { +// updateUi(controller!!.getMedia()) +// } + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + // interrupt position Observer, restart later +// cardViewSeek.scaleX = .8f +// cardViewSeek.scaleY = .8f +// cardViewSeek.animate() +// ?.setInterpolator(FastOutSlowInInterpolator()) +// ?.alpha(1f)?.scaleX(1f)?.scaleY(1f) +// ?.setDuration(200) +// ?.start() + } + + @OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) { + if (controller != null) { + val prog: Float = seekBar.progress / (seekBar.max.toFloat()) + controller!!.seekTo((prog * controller!!.duration).toInt()) + } +// cardViewSeek.scaleX = 1f +// cardViewSeek.scaleY = 1f +// cardViewSeek.animate() +// ?.setInterpolator(FastOutSlowInInterpolator()) +// ?.alpha(0f)?.scaleX(.8f)?.scaleY(.8f) +// ?.setDuration(200) +// ?.start() + } + @UnstableApi private fun updateUi(media: Playable?) { if (media == null) { return } (activity as MainActivity).setPlayerVisible(true) - txtvTitle.text = media.getEpisodeTitle() - feedName.text = media.getFeedTitle() +// txtvTitle.text = media.getEpisodeTitle() +// feedName.text = media.getFeedTitle() onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration())) val options = RequestOptions() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemFragment.kt index db199907..cb6dc842 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ItemFragment.kt @@ -97,6 +97,7 @@ class ItemFragment : Fragment() { @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) val viewBinding = FeeditemFragmentBinding.inflate(inflater) + root = viewBinding.root Log.d(TAG, "fragment onCreateView") txtvPodcast = viewBinding.txtvPodcast @@ -110,8 +111,8 @@ class ItemFragment : Fragment() { txtvTitle.ellipsize = TextUtils.TruncateAt.END webvDescription = viewBinding.webvDescription webvDescription.setTimecodeSelectedListener { time: Int? -> - if (controller != null && item != null && item!!.media != null && controller!!.getMedia() != null && - item!!.media!!.getIdentifier() == controller!!.getMedia()!!.getIdentifier()) { + val cMedia = controller?.getMedia() + if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) { controller!!.seekTo(time ?: 0) } else { (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position, @@ -378,8 +379,7 @@ class ItemFragment : Fragment() { itemsLoaded = true }, { error: Throwable? -> - Log.e(TAG, - Log.getStackTraceString(error)) + Log.e(TAG, Log.getStackTraceString(error)) }) } @@ -387,7 +387,7 @@ class ItemFragment : Fragment() { val feedItem: FeedItem? = DBReader.getFeedItem(itemId) val context = context if (feedItem != null && context != null) { - val duration = if (feedItem.media != null) feedItem.media!!.getDuration() else Int.MAX_VALUE + val duration = feedItem.media?.getDuration()?: Int.MAX_VALUE DBReader.loadDescriptionOfFeedItem(feedItem) val t = ShownotesCleaner(context, feedItem.description?:"", duration) webviewData = t.processShownotes() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index d64c8e13..a1f1c748 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -52,8 +52,8 @@ import kotlin.math.max class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChangeListener { private var navDrawerData: NavDrawerData? = null - private var flatItemList: List? = null - private var contextPressedItem: NavDrawerData.DrawerItem? = null + private var flatItemList: List? = null + private var contextPressedItem: NavDrawerData.FeedDrawerItem? = null private var disposable: Disposable? = null private lateinit var navAdapter: NavListAdapter @@ -140,28 +140,20 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange val inflater: MenuInflater = requireActivity().menuInflater if (contextPressedItem != null) { menu.setHeaderTitle(contextPressedItem!!.title) - if (contextPressedItem!!.type == NavDrawerData.DrawerItem.Type.FEED) { - inflater.inflate(R.menu.nav_feed_context, menu) - // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones! - } else { - inflater.inflate(R.menu.nav_folder_context, menu) - } + inflater.inflate(R.menu.nav_feed_context, menu) + // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones! } MenuItemUtils.setOnClickListeners(menu ) { item: MenuItem -> this.onContextItemSelected(item) } } override fun onContextItemSelected(item: MenuItem): Boolean { - val pressedItem: NavDrawerData.DrawerItem? = contextPressedItem + val pressedItem: NavDrawerData.FeedDrawerItem? = contextPressedItem contextPressedItem = null if (pressedItem == null) { return false } - return if (pressedItem.type == NavDrawerData.DrawerItem.Type.FEED) { - onFeedContextMenuClicked((pressedItem as NavDrawerData.FeedDrawerItem).feed, item) - } else { - onTagContextMenuClicked(pressedItem, item) - } + return onFeedContextMenuClicked(pressedItem.feed, item) } @OptIn(UnstableApi::class) private fun onFeedContextMenuClicked(feed: Feed, item: MenuItem): Boolean { @@ -202,7 +194,7 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange } } - private fun onTagContextMenuClicked(drawerItem: NavDrawerData.DrawerItem?, item: MenuItem): Boolean { + private fun onTagContextMenuClicked(drawerItem: NavDrawerData.FeedDrawerItem?, item: MenuItem): Boolean { val itemId = item.itemId if (itemId == R.id.rename_folder_item) { RenameItemDialog(activity as Activity, drawerItem).show() @@ -245,7 +237,7 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange 0 } - override fun getItem(position: Int): NavDrawerData.DrawerItem? { + override fun getItem(position: Int): NavDrawerData.FeedDrawerItem? { return if (flatItemList != null && 0 <= position && position < flatItemList!!.size) { flatItemList!![position] } else { @@ -260,11 +252,9 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange } else if (StringUtils.isNumeric(lastNavFragment)) { // last fragment was not a list, but a feed val feedId = lastNavFragment.toLong() if (navDrawerData != null) { - val itemToCheck: NavDrawerData.DrawerItem = flatItemList!![position - navAdapter.subscriptionOffset] - if (itemToCheck.type == NavDrawerData.DrawerItem.Type.FEED) { - // When the same feed is displayed multiple times, it should be highlighted multiple times. - return (itemToCheck as NavDrawerData.FeedDrawerItem).feed.id == feedId - } + val itemToCheck: NavDrawerData.FeedDrawerItem = flatItemList!![position - navAdapter.subscriptionOffset] + // When the same feed is displayed multiple times, it should be highlighted multiple times. + return itemToCheck.feed.id == feedId } } return false @@ -303,35 +293,10 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) } else { val pos: Int = position - navAdapter.subscriptionOffset - val clickedItem: NavDrawerData.DrawerItem = flatItemList!![pos] - - if (clickedItem.type == NavDrawerData.DrawerItem.Type.FEED) { - val feedId: Long = (clickedItem as NavDrawerData.FeedDrawerItem).feed.id - (activity as MainActivity).loadFeedFragmentById(feedId, null) - (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) - } else { -// val folder: NavDrawerData.TagDrawerItem = (clickedItem as NavDrawerData.TagDrawerItem) -// if (openFolders.contains(folder.name)) { -// openFolders.remove(folder.name) -// } else { -// openFolders.add(folder.name) -// } -// -// context!!.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) -// .edit() -// .putStringSet(PREF_OPEN_FOLDERS, openFolders) -// .apply() -// -// disposable = -// Observable.fromCallable { makeFlatDrawerData(navDrawerData!!.items, 0) } -// .subscribeOn(Schedulers.computation()) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe( -// { result: List? -> -// flatItemList = result -// navAdapter.notifyDataSetChanged() -// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - } + val clickedItem: NavDrawerData.FeedDrawerItem = flatItemList!![pos] + val feedId: Long = clickedItem.feed.id + (activity as MainActivity).loadFeedFragmentById(feedId, null) + (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) } } else if (UserPreferences.subscriptionsFilter.isEnabled && navAdapter.showSubscriptionList) { @@ -370,7 +335,7 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { result: Pair> -> + { result: Pair> -> navDrawerData = result.first flatItemList = result.second navAdapter.notifyDataSetChanged() @@ -381,18 +346,11 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange }) } - private fun makeFlatDrawerData(items: List, layer: Int): List { - val flatItems: MutableList = ArrayList() + private fun makeFlatDrawerData(items: List, layer: Int): List { + val flatItems: MutableList = ArrayList() for (item in items) { item.layer = layer flatItems.add(item) -// if (item.type == NavDrawerData.DrawerItem.Type.TAG) { -// val folder: NavDrawerData.TagDrawerItem = (item as NavDrawerData.TagDrawerItem) -// folder.isOpen = openFolders.contains(folder.name) -// if (folder.isOpen) { -// flatItems.addAll(makeFlatDrawerData(item.children, layer + 1)) -// } -// } } return flatItems } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt index d227cab5..9dcd081a 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt @@ -124,7 +124,7 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select subscriptionRecycler.adapter = subscriptionAdapter setupEmptyView() - tags.add("None") + tags.add("Untagged") tags.add("All") tags.addAll(DBReader.getTags()) val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, tags) @@ -315,10 +315,10 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select } override fun onContextItemSelected(item: MenuItem): Boolean { - val drawerItem: NavDrawerData.DrawerItem = subscriptionAdapter.getSelectedItem() ?: return false + val drawerItem: NavDrawerData.FeedDrawerItem = subscriptionAdapter.getSelectedItem() ?: return false val itemId = item.itemId - val feed: Feed = (drawerItem as NavDrawerData.FeedDrawerItem).feed + val feed: Feed = drawerItem.feed if (itemId == R.id.multi_select) { speedDialView.visibility = View.VISIBLE return subscriptionAdapter.onContextItemSelected(item) @@ -328,6 +328,7 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select @Subscribe(threadMode = ThreadMode.MAIN) fun onFeedListChanged(event: FeedListUpdateEvent?) { + DBReader.updateFeedList() loadSubscriptions() } @@ -343,7 +344,7 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select } override fun onStartSelectMode() { - val feedsOnly: MutableList = ArrayList() + val feedsOnly: MutableList = ArrayList() for (item in feedListFiltered) { feedsOnly.add(item) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt b/app/src/main/java/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt index e5d7e267..b9ef9db9 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt @@ -1,15 +1,14 @@ package ac.mdiq.podcini.ui.view +import ac.mdiq.podcini.databinding.PlaybackSpeedSeekBarBinding import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.SeekBar import android.widget.SeekBar.OnSeekBarChangeListener import androidx.core.util.Consumer -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.PlaybackSpeedSeekBarBinding -import android.view.LayoutInflater class PlaybackSpeedSeekBar : FrameLayout { private lateinit var binding: PlaybackSpeedSeekBarBinding @@ -30,25 +29,20 @@ class PlaybackSpeedSeekBar : FrameLayout { } private fun setup() { - val inflater = LayoutInflater.from(context) - binding = PlaybackSpeedSeekBarBinding.inflate(inflater, this, false) + binding = PlaybackSpeedSeekBarBinding.inflate(LayoutInflater.from(context), this, true) seekBar = binding.playbackSpeed - binding.butDecSpeed.setOnClickListener { v: View? -> seekBar.progress -= 2 } - binding.butIncSpeed.setOnClickListener { v: View? -> seekBar.progress += 2 } + binding.butDecSpeed.setOnClickListener { seekBar.progress -= 2 } + binding.butIncSpeed.setOnClickListener { seekBar.progress += 2 } seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { val playbackSpeed = (progress + 10) / 20.0f - if (progressChangedListener != null) { - progressChangedListener!!.accept(playbackSpeed) - } + progressChangedListener?.accept(playbackSpeed) } - override fun onStartTrackingTouch(seekBar: SeekBar) { - } + override fun onStartTrackingTouch(seekBar: SeekBar) {} - override fun onStopTrackingTouch(seekBar: SeekBar) { - } + override fun onStopTrackingTouch(seekBar: SeekBar) {} }) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt b/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt index 821ad851..773d7ee2 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt @@ -31,6 +31,7 @@ import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.ui.adapter.actionbutton.ItemActionButton import ac.mdiq.podcini.ui.common.CircularProgressBar import ac.mdiq.podcini.ui.common.ThemeUtils import io.reactivex.functions.Consumer @@ -42,6 +43,7 @@ import kotlin.math.max @UnstableApi class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGroup?) : RecyclerView.ViewHolder(LayoutInflater.from(activity).inflate(R.layout.feeditemlist_item, parent, false)) { + val binding: FeeditemlistItemBinding = FeeditemlistItemBinding.bind(itemView) private val container: View = binding.container @@ -110,7 +112,7 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou isInQueue.visibility = if (item.isTagged(FeedItem.TAG_QUEUE)) View.VISIBLE else View.GONE container.alpha = if (item.isPlayed()) 0.5f else 1.0f - val actionButton: ac.mdiq.podcini.ui.adapter.actionbutton.ItemActionButton = ac.mdiq.podcini.ui.adapter.actionbutton.ItemActionButton.forItem(item) + val actionButton: ItemActionButton = ItemActionButton.forItem(item) actionButton.configure(secondaryActionButton, secondaryActionIcon, activity) secondaryActionButton.isFocusable = false diff --git a/app/src/main/java/ac/mdiq/podcini/util/ChapterUtils.kt b/app/src/main/java/ac/mdiq/podcini/util/ChapterUtils.kt index dd74df11..4ea3ed63 100644 --- a/app/src/main/java/ac/mdiq/podcini/util/ChapterUtils.kt +++ b/app/src/main/java/ac/mdiq/podcini/util/ChapterUtils.kt @@ -32,10 +32,10 @@ object ChapterUtils { @JvmStatic fun getCurrentChapterIndex(media: Playable?, position: Int): Int { - if (media?.getChapters() == null || media.getChapters().isEmpty()) { + val chapters = media?.getChapters() + if (chapters.isNullOrEmpty()) { return -1 } - val chapters = media.getChapters() for (i in chapters.indices) { if (chapters[i].start > position) { return i - 1 @@ -154,7 +154,7 @@ object ChapterUtils { val request: Request = Builder().url(url).cacheControl(cacheControl).build() response = getHttpClient().newCall(request).execute() if (response.isSuccessful && response.body != null) { - return ac.mdiq.podcini.feed.parser.PodcastIndexChapterParser.parse(response.body!!.string()) + return PodcastIndexChapterParser.parse(response.body!!.string()) } } catch (e: IOException) { e.printStackTrace() diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml index ee9c21f7..d39fd9db 100644 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -44,10 +44,9 @@ + app:tint="?android:attr/colorBackground"/> + android:textSize="@dimen/text_size_micro"/> + android:textSize="@dimen/text_size_micro"/> diff --git a/app/src/main/res/layout/edit_tags_dialog.xml b/app/src/main/res/layout/edit_tags_dialog.xml index b20facbf..65728e4d 100644 --- a/app/src/main/res/layout/edit_tags_dialog.xml +++ b/app/src/main/res/layout/edit_tags_dialog.xml @@ -7,11 +7,11 @@ android:orientation="vertical" android:padding="16dp"> - + + + + + + + + + + + + + + + + android:layout_weight="1" + android:orientation="horizontal"> - - - - - - - - - + android:layout_weight="1"> + + + + + + + + + + + + + + + + + + - - - diff --git a/app/src/main/res/layout/speed_select_dialog.xml b/app/src/main/res/layout/speed_select_dialog.xml index ce6ebd3d..4c595208 100644 --- a/app/src/main/res/layout/speed_select_dialog.xml +++ b/app/src/main/res/layout/speed_select_dialog.xml @@ -31,6 +31,33 @@ android:layout_height="wrap_content" android:layout_marginBottom="8dp" /> + + + + + + + + + 16dp 0dp 4dp - 64dp + 100dp 12sp 14sp 16sp @@ -27,7 +27,7 @@ 48dp 64dp - 12dp + 4dp 480dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d14dba05..0ade64b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -824,4 +824,7 @@ Subscription shortcut Select subscription Add shortcut + Episode + Podcast + All