enhancement on tags and player controls

This commit is contained in:
Xilin Jia 2024-02-29 13:53:21 +01:00
parent ee716d6c8e
commit a6f83b71c0
26 changed files with 675 additions and 435 deletions

View File

@ -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<String>
get() {
if (playbackService == null || playbackService!!.audioTracks.isEmpty()) {
if (playbackService?.audioTracks.isNullOrEmpty()) {
return emptyList()
}
return playbackService!!.audioTracks.filterNotNull().map { it }

View File

@ -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<String, String>()

View File

@ -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<String> {
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())
}
}

View File

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

View File

@ -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<String>()
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<Long, Int> = adapter.getFeedCounters(feedCounterSetting)
// getFeedList(adapter)
if (subscriptionsFilter != null) {
// TODO:
if (false || subscriptionsFilter != null) {
feeds = subscriptionsFilter.filter(feeds, feedCounters as Map<Long?, Int>).toMutableList()
}

View File

@ -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<ac.mdiq.podcini.util.event.QueueEvent> = ArrayList()
val events: MutableList<QueueEvent> = ArrayList()
val updatedItems: MutableList<FeedItem> = 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<FeedItem>, events: MutableList<ac.mdiq.podcini.util.event.QueueEvent>) {
private fun applySortOrder(queue: MutableList<FeedItem>, events: MutableList<QueueEvent>) {
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<ac.mdiq.podcini.util.event.QueueEvent> = ArrayList()
val events: MutableList<QueueEvent> = ArrayList()
val updatedItems: MutableList<FeedItem> = 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()
}

View File

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

View File

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

View File

@ -68,17 +68,13 @@ class SelectSubscriptionActivity : AppCompatActivity() {
}
}
fun getFeedItems(items: List<NavDrawerData.DrawerItem?>, result: MutableList<Feed>): List<Feed> {
fun getFeedItems(items: List<NavDrawerData.FeedDrawerItem?>, result: MutableList<Feed>): List<Feed> {
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
}

View File

@ -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<MainActivity> = WeakReference<MainActivity>(mainActivity)
private var listItems: List<NavDrawerData.DrawerItem>
private var selectedItem: NavDrawerData.DrawerItem? = null
private var listItems: List<NavDrawerData.FeedDrawerItem>
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<Feed>()
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<NavDrawerData.DrawerItem>) {
fun setItems(listItems: List<NavDrawerData.FeedDrawerItem>) {
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()

View File

@ -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<String>
@ -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<FeedPreferences>
val commonTags: MutableSet<String> = HashSet(
feedPreferencesList!![0].getTags())
val serializedData: Serializable? = requireArguments().getSerializable(ARG_FEED_PREFERENCES)
val feedPreferencesList = if (serializedData is ArrayList<*>) serializedData.filterIsInstance<FeedPreferences>() else listOf()
// val feedPreferencesList = serializedData as? List<FeedPreferences> ?: listOf()
val commonTags: MutableSet<String> = 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<List<String>> {
// val data = DBReader.getNavDrawerData(null)
// val items = data.items
val folders: MutableList<String> = 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<String> ->
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<FeedPreferences>?, commonTags: Set<String?>) {
if (viewBinding.rootFolderCheckbox.isChecked) {
displayedTags.add(FeedPreferences.TAG_ROOT)
}
for (preferences in feedPreferencesList!!) {
@OptIn(UnstableApi::class) private fun updatePreferencesTags(feedPreferencesList: List<FeedPreferences>, commonTags: Set<String>) {
// if (viewBinding.rootFolderCheckbox.isChecked) {
// displayedTags.add(FeedPreferences.TAG_ROOT)
// }
for (preferences in feedPreferencesList) {
preferences.getTags().removeAll(commonTags)
preferences.getTags().addAll(displayedTags)
DBWriter.setFeedPreferences(preferences)

View File

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

View File

@ -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<Playable> { emitter: MaybeEmitter<Playable?> ->
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())

View File

@ -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<Playable?> { controller?.getMedia() }
disposable = Maybe.fromCallable<Playable?> {
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()

View File

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

View File

@ -52,8 +52,8 @@ import kotlin.math.max
class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChangeListener {
private var navDrawerData: NavDrawerData? = null
private var flatItemList: List<NavDrawerData.DrawerItem>? = null
private var contextPressedItem: NavDrawerData.DrawerItem? = null
private var flatItemList: List<NavDrawerData.FeedDrawerItem>? = 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<NavDrawerData.DrawerItem>? ->
// 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<NavDrawerData, List<NavDrawerData.DrawerItem>> ->
{ result: Pair<NavDrawerData, List<NavDrawerData.FeedDrawerItem>> ->
navDrawerData = result.first
flatItemList = result.second
navAdapter.notifyDataSetChanged()
@ -381,18 +346,11 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange
})
}
private fun makeFlatDrawerData(items: List<NavDrawerData.DrawerItem>, layer: Int): List<NavDrawerData.DrawerItem> {
val flatItems: MutableList<NavDrawerData.DrawerItem> = ArrayList()
private fun makeFlatDrawerData(items: List<NavDrawerData.FeedDrawerItem>, layer: Int): List<NavDrawerData.FeedDrawerItem> {
val flatItems: MutableList<NavDrawerData.FeedDrawerItem> = 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
}

View File

@ -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<NavDrawerData.DrawerItem> = ArrayList<NavDrawerData.DrawerItem>()
val feedsOnly: MutableList<NavDrawerData.FeedDrawerItem> = ArrayList<NavDrawerData.FeedDrawerItem>()
for (item in feedListFiltered) {
feedsOnly.add(item)
}

View File

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

View File

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

View File

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

View File

@ -44,10 +44,9 @@
<ImageView
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_alignBottom="@id/pager"
android:importantForAccessibility="no"
app:srcCompat="@drawable/bg_gradient"
app:tint="?android:attr/colorBackground" />
app:tint="?android:attr/colorBackground"/>
<androidx.cardview.widget.CardView
android:id="@+id/cardViewSeek"
@ -115,7 +114,7 @@
android:layout_marginLeft="16dp"
android:text="@string/position_default_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_micro" />
android:textSize="@dimen/text_size_micro"/>
<ac.mdiq.podcini.ui.view.NoRelayoutTextView
android:id="@+id/txtvLength"
@ -129,7 +128,7 @@
android:background="?android:attr/selectableItemBackground"
android:text="@string/position_default_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_micro" />
android:textSize="@dimen/text_size_micro"/>
</RelativeLayout>

View File

@ -7,11 +7,11 @@
android:orientation="vertical"
android:padding="16dp">
<CheckBox
android:id="@+id/rootFolderCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/feed_folders_include_root" />
<!-- <CheckBox-->
<!-- android:id="@+id/rootFolderCheckbox"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="@string/feed_folders_include_root" />-->
<com.joanzapata.iconify.widget.IconTextView
android:id="@+id/commonTagsInfo"

View File

@ -9,71 +9,190 @@
android:background="?attr/selectableItemBackground"
android:orientation="vertical">
<ac.mdiq.podcini.ui.view.ChapterSeekBar
android:id="@+id/sbPosition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5.dp"
android:clickable="true"
android:max="500"
tools:progress="100" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ac.mdiq.podcini.ui.view.NoRelayoutTextView
android:id="@+id/txtvPosition"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="5dp"
android:layout_marginLeft="5dp"
android:text="@string/position_default_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_micro"/>
<ac.mdiq.podcini.ui.view.NoRelayoutTextView
android:id="@+id/txtvLength"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
android:textAlignment="textEnd"
android:background="?android:attr/selectableItemBackground"
android:text="@string/position_default_label"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_micro"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center_vertical"
android:layout_weight="1">
android:layout_weight="1"
android:orientation="horizontal">
<ImageView
android:id="@+id/imgvCover"
android:layout_width="wrap_content"
android:layout_width="70dp"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:cropToPadding="true"
android:maxWidth="96dp"
android:scaleType="fitCenter"
android:background="@color/non_square_icon_background"
tools:src="@tools:sample/avatars" />
<LinearLayout
<RelativeLayout
android:id="@+id/player_control"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp">
<TextView
android:id="@+id/txtvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
style="@style/Base.TextAppearance.AppCompat.Body1"
tools:text="Episode title that is too long and will cause the text to wrap" />
<TextView
android:id="@+id/txtvAuthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="end"
android:maxLines="1"
style="@style/TextAppearance.AppCompat.Body1"
tools:text="Episode author that is too long and will cause the text to wrap" />
</LinearLayout>
<ac.mdiq.podcini.ui.view.PlayButton
android:id="@+id/butPlay"
android:layout_width="52dp"
android:layout_height="match_parent"
android:contentDescription="@string/pause_label"
android:background="?attr/selectableItemBackground"
android:scaleType="fitCenter"
android:padding="8dp"
app:srcCompat="@drawable/ic_play_48dp"
tools:src="@drawable/ic_play_48dp" />
android:layout_weight="1">
<ac.mdiq.podcini.ui.view.PlayButton
android:id="@+id/butPlay"
android:layout_width="@dimen/audioplayer_playercontrols_length_big"
android:layout_height="@dimen/audioplayer_playercontrols_length_big"
android:layout_centerHorizontal="true"
android:layout_centerVertical="false"
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pause_label"
android:padding="8dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_play_48dp"
tools:srcCompat="@drawable/ic_play_48dp" />
<ImageButton
android:id="@+id/butRev"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="false"
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
android:layout_toStartOf="@id/butPlay"
android:layout_toLeftOf="@id/butPlay"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/rewind_label"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fast_rewind"
tools:srcCompat="@drawable/ic_fast_rewind" />
<TextView
android:id="@+id/txtvRev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butRev"
android:layout_alignStart="@id/butRev"
android:layout_alignLeft="@id/butRev"
android:layout_alignEnd="@id/butRev"
android:layout_alignRight="@id/butRev"
android:clickable="false"
android:gravity="center"
android:text="30"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView
android:id="@+id/butPlaybackSpeed"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="false"
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
android:layout_toStartOf="@id/butRev"
android:layout_toLeftOf="@id/butRev"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/playback_speed"
app:foregroundColor="?attr/action_icon_color"
tools:srcCompat="@drawable/ic_playback_speed" />
<TextView
android:id="@+id/txtvPlaybackSpeed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butPlaybackSpeed"
android:layout_alignStart="@id/butPlaybackSpeed"
android:layout_alignLeft="@id/butPlaybackSpeed"
android:layout_alignEnd="@id/butPlaybackSpeed"
android:layout_alignRight="@id/butPlaybackSpeed"
android:clickable="false"
android:gravity="center"
android:text="1.00"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<ImageButton
android:id="@+id/butFF"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="false"
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
android:layout_toEndOf="@id/butPlay"
android:layout_toRightOf="@id/butPlay"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/fast_forward_label"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_fast_forward"
tools:srcCompat="@drawable/ic_fast_forward" />
<TextView
android:id="@+id/txtvFF"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butFF"
android:layout_alignStart="@id/butFF"
android:layout_alignLeft="@id/butFF"
android:layout_alignEnd="@id/butFF"
android:layout_alignRight="@id/butFF"
android:clickable="false"
android:gravity="center"
android:text="30"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
<ImageButton
android:id="@+id/butSkip"
android:layout_width="@dimen/audioplayer_playercontrols_length"
android:layout_height="@dimen/audioplayer_playercontrols_length"
android:layout_centerVertical="false"
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
android:layout_toEndOf="@id/butFF"
android:layout_toRightOf="@id/butFF"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/skip_episode_label"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_skip_48dp"
tools:srcCompat="@drawable/ic_skip_48dp" />
</RelativeLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/episodeProgress"
android:layout_width="match_parent"
android:layout_height="4dp"
android:indeterminate="false"
style="?android:attr/progressBarStyleHorizontal"
tools:progress="100" />
</LinearLayout>

View File

@ -31,6 +31,33 @@
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/propertyOf"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<RadioButton
android:id="@+id/for_episode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Episode"
android:checked="true"/>
<RadioButton
android:id="@+id/for_podcast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Podcast" />
<RadioButton
android:id="@+id/for_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/All" />
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@ -5,7 +5,7 @@
<dimen name="drawer_corner_size">16dp</dimen>
<dimen name="widget_margin">0dp</dimen>
<dimen name="widget_inner_radius">4dp</dimen>
<dimen name="external_player_height">64dp</dimen>
<dimen name="external_player_height">100dp</dimen>
<dimen name="text_size_micro">12sp</dimen>
<dimen name="text_size_small">14sp</dimen>
<dimen name="text_size_navdrawer">16sp</dimen>
@ -27,7 +27,7 @@
<dimen name="audioplayer_playercontrols_length">48dp</dimen>
<dimen name="audioplayer_playercontrols_length_big">64dp</dimen>
<dimen name="audioplayer_playercontrols_margin">12dp</dimen>
<dimen name="audioplayer_playercontrols_margin">4dp</dimen>
<dimen name="nav_drawer_max_screen_size">480dp</dimen>
</resources>

View File

@ -824,4 +824,7 @@
<string name="shortcut_subscription_label">Subscription shortcut</string>
<string name="shortcut_select_subscription">Select subscription</string>
<string name="add_shortcut">Add shortcut</string>
<string name="Episode">Episode</string>
<string name="Podcast">Podcast</string>
<string name="All">All</string>
</resources>