4.5.0 commit

This commit is contained in:
Xilin Jia 2024-04-01 22:15:50 +00:00
parent 725c15fbba
commit 20d0038eee
34 changed files with 423 additions and 63 deletions

View File

@ -36,6 +36,19 @@ Other notable features and changes include:
* setting for a feed: either use global or customized * setting for a feed: either use global or customized
* setting at the player: set for current playing and save for global * setting at the player: set for current playing and save for global
* customized feed setting takes precedence when playing an episode * customized feed setting takes precedence when playing an episode
* Added preference "Fast Forward Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 10.0)
* The "Skip to next episode" button on the player
* long-press moves to the next episode
* by default, single tap does nothing
* if the user customize "Fast Forward Speed" to a value greater than 0.1, it behaves in the following way:
* single tap during play, the set speed is used to play the current audio
* single tap again, the original play speed resumes
* single tap not during play has no effect
* Added preference "Fallback Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 1.5)
* the Play button on the player
* by default, it behaves the same as usual
* if the user customize "Fallback speed" to a value greater than 0.1, long-press the button during play enters the fallback mode and plays at the set fallback speed, single tap exits the fallback mode
* Subscriptions view has sorting by "Unread publication date"
* Feed info view offers a link for direct search of feeds related to author * Feed info view offers a link for direct search of feeds related to author
* More info about feeds are shown in the online search view * More info about feeds are shown in the online search view
* Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes * Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes

View File

@ -149,8 +149,8 @@ android {
// Version code schema (not used): // Version code schema (not used):
// "1.2.3-beta4" -> 1020304 // "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395 // "1.2.3" -> 1020395
versionCode 3020119 versionCode 3020120
versionName "4.4.3" versionName "4.5.0"
def commit = "" def commit = ""
try { try {

View File

@ -57,7 +57,7 @@ class MainActivityTest {
EspressoTestUtils.openNavDrawer() EspressoTestUtils.openNavDrawer()
Espresso.onView(ViewMatchers.withText(R.string.add_feed_label)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withText(R.string.add_feed_label)).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.addViaUrlButton)).perform(ViewActions.scrollTo(), ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.addViaUrlButton)).perform(ViewActions.scrollTo(), ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.urlEditText)).perform(ViewActions.replaceText(feed.download_url)) Espresso.onView(ViewMatchers.withId(R.id.editText)).perform(ViewActions.replaceText(feed.download_url))
Espresso.onView(ViewMatchers.withText(R.string.confirm_label)) Espresso.onView(ViewMatchers.withText(R.string.confirm_label))
.perform(ViewActions.scrollTo(), ViewActions.click()) .perform(ViewActions.scrollTo(), ViewActions.click())

View File

@ -221,8 +221,8 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
PlayerStatus.PREPARING -> if (playbackService != null) { PlayerStatus.PREPARING -> if (playbackService != null) {
updatePlayButtonShowsPlay(!playbackService!!.isStartWhenPrepared) updatePlayButtonShowsPlay(!playbackService!!.isStartWhenPrepared)
} }
PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED -> updatePlayButtonShowsPlay( PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED ->
true) updatePlayButtonShowsPlay(true)
else -> {} else -> {}
} }
} }
@ -267,7 +267,8 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
return return
} }
when (status) { when (status) {
PlayerStatus.PLAYING -> playbackService?.pause(true, false) PlayerStatus.FALLBACK -> fallbackSpeed(1.0f)
PlayerStatus.PLAYING -> playbackService?.pause(abandonAudioFocus = true, reinit = false)
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> playbackService?.resume() PlayerStatus.PAUSED, PlayerStatus.PREPARED -> playbackService?.resume()
PlayerStatus.PREPARING -> playbackService!!.isStartWhenPrepared = !playbackService!!.isStartWhenPrepared PlayerStatus.PREPARING -> playbackService!!.isStartWhenPrepared = !playbackService!!.isStartWhenPrepared
PlayerStatus.INITIALIZED -> { PlayerStatus.INITIALIZED -> {
@ -345,6 +346,28 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
} }
} }
fun speedForward(speed: Float) {
if (playbackService != null) {
playbackService!!.speedForward(speed)
}
}
fun fallbackSpeed(speed: Float) {
if (playbackService != null) {
when (status) {
PlayerStatus.PLAYING -> {
status = PlayerStatus.FALLBACK
playbackService!!.fallbackSpeed(speed)
}
PlayerStatus.FALLBACK -> {
status = PlayerStatus.PLAYING
playbackService!!.fallbackSpeed(speed)
}
else -> {}
}
}
}
fun setSkipSilence(skipSilence: Boolean) { fun setSkipSilence(skipSilence: Boolean) {
playbackService?.skipSilence(skipSilence) playbackService?.skipSilence(skipSilence)
} }

View File

@ -5,6 +5,7 @@ enum class PlayerStatus(private val statusValue: Int) {
ERROR(-1), ERROR(-1),
PREPARING(19), PREPARING(19),
PAUSED(30), PAUSED(30),
FALLBACK(35),
PLAYING(40), PLAYING(40),
STOPPED(5), STOPPED(5),
PREPARED(20), PREPARED(20),

View File

@ -74,9 +74,11 @@ object UserPreferences {
private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal" private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"
const val PREF_SMART_MARK_AS_PLAYED_SECS: String = "prefSmartMarkAsPlayedSecs" const val PREF_SMART_MARK_AS_PLAYED_SECS: String = "prefSmartMarkAsPlayedSecs"
private const val PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray" private const val PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"
private const val PREF_FALLBACK_SPEED = "prefFallbackSpeed"
const val PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: String = "prefPauseForFocusLoss" const val PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: String = "prefPauseForFocusLoss"
private const val PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed" private const val PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed"
const val PREF_STREAM_OVER_DOWNLOAD: String = "prefStreamOverDownload" const val PREF_STREAM_OVER_DOWNLOAD: String = "prefStreamOverDownload"
private const val PREF_SPEEDFORWRD_SPEED = "prefSpeedforwardSpeed"
// Network // Network
private const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded" private const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"
@ -127,6 +129,8 @@ object UserPreferences {
const val FEED_ORDER_COUNTER: Int = 0 const val FEED_ORDER_COUNTER: Int = 0
const val FEED_ORDER_ALPHABETICAL: Int = 1 const val FEED_ORDER_ALPHABETICAL: Int = 1
const val FEED_ORDER_MOST_PLAYED: Int = 3 const val FEED_ORDER_MOST_PLAYED: Int = 3
const val FEED_ORDER_LAST_UPDATED: Int = 4
const val FEED_ORDER_LAST_UNREAD_UPDATED: Int = 5
const val DEFAULT_PAGE_REMEMBER: String = "remember" const val DEFAULT_PAGE_REMEMBER: String = "remember"
private lateinit var context: Context private lateinit var context: Context
@ -175,7 +179,7 @@ object UserPreferences {
return ArrayList(listOf(*TextUtils.split(hiddenItems, ","))) return ArrayList(listOf(*TextUtils.split(hiddenItems, ",")))
} }
set(items) { set(items) {
val str = TextUtils.join(",", items!!) val str = TextUtils.join(",", items)
prefs.edit() prefs.edit()
.putString(PREF_HIDDEN_DRAWER_ITEMS, str) .putString(PREF_HIDDEN_DRAWER_ITEMS, str)
.apply() .apply()
@ -545,6 +549,40 @@ object UserPreferences {
val isEnableAutodownloadWifiFilter: Boolean val isEnableAutodownloadWifiFilter: Boolean
get() = Build.VERSION.SDK_INT < 29 && prefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false) get() = Build.VERSION.SDK_INT < 29 && prefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false)
@JvmStatic
var speedforwardSpeed: Float
get() {
try {
return prefs.getString(PREF_SPEEDFORWRD_SPEED, "0.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
speedforwardSpeed = 0.0f
return 0.0f
}
}
set(speed) {
prefs.edit()
.putString(PREF_SPEEDFORWRD_SPEED, speed.toString())
.apply()
}
@JvmStatic
var fallbackSpeed: Float
get() {
try {
return prefs.getString(PREF_FALLBACK_SPEED, "0.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
fallbackSpeed = 0.0f
return 0.0f
}
}
set(speed) {
prefs.edit()
.putString(PREF_FALLBACK_SPEED, speed.toString())
.apply()
}
@JvmStatic @JvmStatic
var fastForwardSecs: Int var fastForwardSecs: Int
get() = prefs.getInt(PREF_FAST_FORWARD_SECS, 30) get() = prefs.getInt(PREF_FAST_FORWARD_SECS, 30)

View File

@ -138,6 +138,10 @@ class PlaybackService : MediaBrowserServiceCompat() {
private var clickCount = 0 private var clickCount = 0
private val clickHandler = Handler(Looper.getMainLooper()) private val clickHandler = Handler(Looper.getMainLooper())
private var isSpeedForward = false
private var normalSpeed = 1.0f
private var isFallbackSpeed = false
/** /**
* Used for Lollipop notifications, Android Wear, and Android Auto. * Used for Lollipop notifications, Android Wear, and Android Auto.
*/ */
@ -224,7 +228,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
var wasPlaying = false var wasPlaying = false
if (mediaPlayer != null) { if (mediaPlayer != null) {
media = mediaPlayer!!.getPlayable() media = mediaPlayer!!.getPlayable()
wasPlaying = mediaPlayer!!.playerStatus == PlayerStatus.PLAYING wasPlaying = mediaPlayer!!.playerStatus == PlayerStatus.PLAYING || mediaPlayer!!.playerStatus == PlayerStatus.FALLBACK
mediaPlayer!!.pause(true, false) mediaPlayer!!.pause(true, false)
mediaPlayer!!.shutdown() mediaPlayer!!.shutdown()
} }
@ -242,7 +246,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
super.onDestroy() super.onDestroy()
Log.d(TAG, "Service is about to be destroyed") Log.d(TAG, "Service is about to be destroyed")
if (notificationBuilder.playerStatus == PlayerStatus.PLAYING) { if (notificationBuilder.playerStatus == PlayerStatus.PLAYING || notificationBuilder.playerStatus == PlayerStatus.FALLBACK) {
notificationBuilder.playerStatus = PlayerStatus.STOPPED notificationBuilder.playerStatus = PlayerStatus.STOPPED
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this,
@ -618,7 +622,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
status == PlayerStatus.PLAYING -> { status == PlayerStatus.PLAYING -> {
mediaPlayer?.pause(!isPersistNotify, false) mediaPlayer?.pause(!isPersistNotify, false)
} }
status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> { status == PlayerStatus.FALLBACK || status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> {
mediaPlayer?.resume() mediaPlayer?.resume()
} }
status == PlayerStatus.PREPARING -> { status == PlayerStatus.PREPARING -> {
@ -678,7 +682,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
} }
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> { KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { if (this.status == PlayerStatus.FALLBACK || this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) {
mediaPlayer?.seekDelta(fastForwardSecs * 1000) mediaPlayer?.seekDelta(fastForwardSecs * 1000)
return true return true
} }
@ -690,7 +694,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
// Handle remapped button as notification button which is not remapped again. // Handle remapped button as notification button which is not remapped again.
return handleKeycode(hardwarePreviousButton, true) return handleKeycode(hardwarePreviousButton, true)
} }
this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED -> { this.status == PlayerStatus.FALLBACK || this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED -> {
mediaPlayer?.seekTo(0) mediaPlayer?.seekTo(0)
return true return true
} }
@ -698,14 +702,14 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
} }
KeyEvent.KEYCODE_MEDIA_REWIND -> { KeyEvent.KEYCODE_MEDIA_REWIND -> {
if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { if (this.status == PlayerStatus.FALLBACK || this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) {
mediaPlayer?.seekDelta(-rewindSecs * 1000) mediaPlayer?.seekDelta(-rewindSecs * 1000)
return true return true
} }
return false return false
} }
KeyEvent.KEYCODE_MEDIA_STOP -> { KeyEvent.KEYCODE_MEDIA_STOP -> {
if (status == PlayerStatus.PLAYING) { if (this.status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING) {
mediaPlayer?.pause(true, true) mediaPlayer?.pause(true, true)
} }
@ -714,7 +718,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
else -> { else -> {
Log.d(TAG, "Unhandled key code: $keycode") Log.d(TAG, "Unhandled key code: $keycode")
if (info?.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something if (info?.playable != null && info.playerStatus == PlayerStatus.PLAYING) {
// only notify the user about an unknown key event if it is actually doing something
val message = String.format(resources.getString(R.string.unknown_media_key), keycode) val message = String.format(resources.getString(R.string.unknown_media_key), keycode)
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
} }
@ -933,7 +938,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused") @Suppress("unused")
fun playerError(event: PlayerErrorEvent?) { fun playerError(event: PlayerErrorEvent?) {
if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING) { if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK) {
mediaPlayer!!.pause(true, false) mediaPlayer!!.pause(true, false)
} }
stateManager.stopService() stateManager.stopService()
@ -1176,6 +1181,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
val state = if (playerStatus != null) { val state = if (playerStatus != null) {
when (playerStatus) { when (playerStatus) {
PlayerStatus.PLAYING -> PlaybackStateCompat.STATE_PLAYING PlayerStatus.PLAYING -> PlaybackStateCompat.STATE_PLAYING
PlayerStatus.FALLBACK -> PlaybackStateCompat.STATE_PLAYING
PlayerStatus.PREPARED, PlayerStatus.PAUSED -> PlaybackStateCompat.STATE_PAUSED PlayerStatus.PREPARED, PlayerStatus.PAUSED -> PlaybackStateCompat.STATE_PAUSED
PlayerStatus.STOPPED -> PlaybackStateCompat.STATE_STOPPED PlayerStatus.STOPPED -> PlaybackStateCompat.STATE_STOPPED
PlayerStatus.SEEKING -> PlaybackStateCompat.STATE_FAST_FORWARDING PlayerStatus.SEEKING -> PlaybackStateCompat.STATE_FAST_FORWARDING
@ -1401,7 +1407,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) { private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) {
var isPlaying = false var isPlaying = false
if (info?.playerStatus == PlayerStatus.PLAYING) { if (info?.playerStatus == PlayerStatus.PLAYING || info?.playerStatus == PlayerStatus.FALLBACK) {
isPlaying = true isPlaying = true
} }
@ -1505,7 +1511,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
*/ */
private fun pauseIfPauseOnDisconnect() { private fun pauseIfPauseOnDisconnect() {
Log.d(TAG, "pauseIfPauseOnDisconnect()") Log.d(TAG, "pauseIfPauseOnDisconnect()")
transientPause = (mediaPlayer?.playerStatus == PlayerStatus.PLAYING) transientPause = (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK)
if (isPauseOnHeadsetDisconnect && !isCasting) { if (isPauseOnHeadsetDisconnect && !isCasting) {
mediaPlayer?.pause(!isPersistNotify, false) mediaPlayer?.pause(!isPersistNotify, false)
} }
@ -1598,6 +1604,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
fun pause(abandonAudioFocus: Boolean, reinit: Boolean) { fun pause(abandonAudioFocus: Boolean, reinit: Boolean) {
mediaPlayer?.pause(abandonAudioFocus, reinit) mediaPlayer?.pause(abandonAudioFocus, reinit)
isSpeedForward = false
isFallbackSpeed = false
} }
val pSMPInfo: PSMPInfo val pSMPInfo: PSMPInfo
@ -1610,6 +1618,9 @@ class PlaybackService : MediaBrowserServiceCompat() {
get() = mediaPlayer?.getPlayable() get() = mediaPlayer?.getPlayable()
fun setSpeed(speed: Float) { fun setSpeed(speed: Float) {
isSpeedForward = false
isFallbackSpeed = false
currentlyPlayingTemporaryPlaybackSpeed = speed currentlyPlayingTemporaryPlaybackSpeed = speed
if (currentMediaType == MediaType.VIDEO) { if (currentMediaType == MediaType.VIDEO) {
videoPlaybackSpeed = speed videoPlaybackSpeed = speed
@ -1620,6 +1631,30 @@ class PlaybackService : MediaBrowserServiceCompat() {
mediaPlayer?.setPlaybackParams(speed, isSkipSilence) mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
} }
fun speedForward(speed: Float) {
if (mediaPlayer == null || isFallbackSpeed) return
if (!isSpeedForward) {
normalSpeed = mediaPlayer!!.getPlaybackSpeed()
mediaPlayer!!.setPlaybackParams(speed, isSkipSilence)
} else {
mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence)
}
isSpeedForward = !isSpeedForward
}
fun fallbackSpeed(speed: Float) {
if (mediaPlayer == null || isSpeedForward) return
if (!isFallbackSpeed) {
normalSpeed = mediaPlayer!!.getPlaybackSpeed()
mediaPlayer!!.setPlaybackParams(speed, isSkipSilence)
} else {
mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence)
}
isFallbackSpeed = !isFallbackSpeed
}
fun skipSilence(skipSilence: Boolean) { fun skipSilence(skipSilence: Boolean) {
mediaPlayer?.setPlaybackParams(currentPlaybackSpeed, skipSilence) mediaPlayer?.setPlaybackParams(currentPlaybackSpeed, skipSilence)
} }
@ -1800,13 +1835,13 @@ class PlaybackService : MediaBrowserServiceCompat() {
override fun onFastForward() { override fun onFastForward() {
Log.d(TAG, "onFastForward()") Log.d(TAG, "onFastForward()")
// speedForward(2.5f)
seekDelta(fastForwardSecs * 1000) seekDelta(fastForwardSecs * 1000)
} }
override fun onSkipToNext() { override fun onSkipToNext() {
Log.d(TAG, "onSkipToNext()") Log.d(TAG, "onSkipToNext()")
val uiModeManager = applicationContext val uiModeManager = applicationContext.getSystemService(UI_MODE_SERVICE) as UiModeManager
.getSystemService(UI_MODE_SERVICE) as UiModeManager
if (hardwareForwardButton == KeyEvent.KEYCODE_MEDIA_NEXT if (hardwareForwardButton == KeyEvent.KEYCODE_MEDIA_NEXT
|| uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) { || uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) {
mediaPlayer?.skip() mediaPlayer?.skip()

View File

@ -867,17 +867,17 @@ object DBReader {
// getFeedList(adapter) // getFeedList(adapter)
// TODO: // TODO:
if (false && subscriptionsFilter != null) { // if (false && subscriptionsFilter != null) {
feeds = subscriptionsFilter.filter(feeds, feedCounters as Map<Long?, Int>).toMutableList() // feeds = subscriptionsFilter.filter(feeds, feedCounters as Map<Long?, Int>).toMutableList()
} // }
val comparator: Comparator<Feed> val comparator: Comparator<Feed>
val feedOrder = feedOrder val feedOrder = feedOrder
when (feedOrder) { when (feedOrder) {
UserPreferences.FEED_ORDER_COUNTER -> { UserPreferences.FEED_ORDER_COUNTER -> {
comparator = Comparator { lhs: Feed, rhs: Feed -> comparator = Comparator { lhs: Feed, rhs: Feed ->
val counterLhs = (if (feedCounters.containsKey(lhs.id)) feedCounters[lhs.id] else 0)!!.toLong() val counterLhs = (if (feedCounters.containsKey(lhs.id)) feedCounters[lhs.id]!! else 0).toLong()
val counterRhs = (if (feedCounters.containsKey(rhs.id)) feedCounters[rhs.id] else 0)!!.toLong() val counterRhs = (if (feedCounters.containsKey(rhs.id)) feedCounters[rhs.id]!! else 0).toLong()
when { when {
counterLhs > counterRhs -> { counterLhs > counterRhs -> {
// reverse natural order: podcast with most unplayed episodes first // reverse natural order: podcast with most unplayed episodes first
@ -929,7 +929,7 @@ object DBReader {
} }
} }
} }
else -> { UserPreferences.FEED_ORDER_LAST_UPDATED -> {
val recentPubDates = adapter.mostRecentItemDates val recentPubDates = adapter.mostRecentItemDates
comparator = Comparator { lhs: Feed, rhs: Feed -> comparator = Comparator { lhs: Feed, rhs: Feed ->
val dateLhs = if (recentPubDates.containsKey(lhs.id)) recentPubDates[lhs.id]!! else 0 val dateLhs = if (recentPubDates.containsKey(lhs.id)) recentPubDates[lhs.id]!! else 0
@ -937,6 +937,14 @@ object DBReader {
dateRhs.compareTo(dateLhs) dateRhs.compareTo(dateLhs)
} }
} }
else -> {
val recentUnreadPubDates = adapter.mostRecentUnreadItemDates
comparator = Comparator { lhs: Feed, rhs: Feed ->
val dateLhs = if (recentUnreadPubDates.containsKey(lhs.id)) recentUnreadPubDates[lhs.id]!! else 0
val dateRhs = if (recentUnreadPubDates.containsKey(rhs.id)) recentUnreadPubDates[rhs.id]!! else 0
dateRhs.compareTo(dateLhs)
}
}
} }
feeds.sortWith(comparator) feeds.sortWith(comparator)

View File

@ -991,6 +991,27 @@ class PodDBAdapter private constructor() {
return result return result
} }
val mostRecentUnreadItemDates: Map<Long, Long>
get() {
val query = ("SELECT " + KEY_FEED + ","
+ " MAX(" + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ") AS most_recent_pubdate"
+ " FROM " + TABLE_NAME_FEED_ITEMS
+ " WHERE " + KEY_READ + " = 0"
+ " GROUP BY " + KEY_FEED)
val c = db.rawQuery(query, null)
val result: MutableMap<Long, Long> = HashMap()
if (c.moveToFirst()) {
do {
val feedId = c.getLong(0)
val date = c.getLong(1)
result[feedId] = date
} while (c.moveToNext())
}
c.close()
return result
}
/** /**
* Uses DatabaseUtils to escape a search query and removes ' at the * Uses DatabaseUtils to escape a search query and removes ' at the
* beginning and the end of the string returned by the escape method. * beginning and the end of the string returned by the escape method.

View File

@ -102,21 +102,12 @@ class Feed : FeedFile {
var itemFilter: FeedItemFilter? = null var itemFilter: FeedItemFilter? = null
private set private set
/**
* User-preferred sortOrder for display.
* Only those of scope [SortOrder.Scope.INTRA_FEED] is allowed.
*/
var sortOrder: SortOrder? = null var sortOrder: SortOrder? = null
set(sortOrder) { set(sortOrder) {
if (!(sortOrder != null && sortOrder.scope != SortOrder.Scope.INTRA_FEED)) { if (sortOrder == null) {
Log.w("Feed sortOrder", "The specified sortOrder " + sortOrder Log.w("Feed sortOrder", "The specified sortOrder $sortOrder is invalid.")
+ " is invalid. Only those with INTRA_FEED scope are allowed.") return
} }
// This looks suicidal:
// require(!(sortOrder != null && sortOrder.scope != SortOrder.Scope.INTRA_FEED)) {
// ("The specified sortOrder " + sortOrder
// + " is invalid. Only those with INTRA_FEED scope are allowed.")
// }
field = sortOrder field = sortOrder
} }

View File

@ -16,12 +16,14 @@ enum class SortOrder(@JvmField val code: Int, @JvmField val scope: Scope) {
SIZE_LARGE_SMALL(10, Scope.INTRA_FEED), SIZE_LARGE_SMALL(10, Scope.INTRA_FEED),
FEED_TITLE_A_Z(101, Scope.INTER_FEED), FEED_TITLE_A_Z(101, Scope.INTER_FEED),
FEED_TITLE_Z_A(102, Scope.INTER_FEED), FEED_TITLE_Z_A(102, Scope.INTER_FEED),
RANDOM(103, Scope.INTER_FEED), RANDOM(103, Scope.INTER_FEED),
SMART_SHUFFLE_OLD_NEW(104, Scope.INTER_FEED), SMART_SHUFFLE_OLD_NEW(104, Scope.INTER_FEED),
SMART_SHUFFLE_NEW_OLD(105, Scope.INTER_FEED); SMART_SHUFFLE_NEW_OLD(105, Scope.INTER_FEED);
enum class Scope { enum class Scope {
INTRA_FEED, INTER_FEED INTRA_FEED,
INTER_FEED
} }
companion object { companion object {

View File

@ -0,0 +1,44 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.EditTextDialogBinding
import ac.mdiq.podcini.preferences.UserPreferences.fallbackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.speedforwardSpeed
import android.app.Activity
import android.content.DialogInterface
import android.text.Editable
import android.text.InputType
import android.view.LayoutInflater
import androidx.media3.common.util.UnstableApi
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
@UnstableApi
class EditFallbackSpeedDialog(activity: Activity) {
private val activityRef = WeakReference(activity)
fun show() {
val activity = activityRef.get() ?: return
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
binding.editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
binding.editText.text = Editable.Factory.getInstance().newEditable(fallbackSpeed.toString())
MaterialAlertDialogBuilder(activity)
.setView(binding.root)
.setTitle(R.string.edit_fast_forward_speed)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
var speed = binding.editText.text.toString().toFloatOrNull() ?: 0.0f
when {
speed < 0.0f -> speed = 0.0f
speed > 1.5f -> speed = 1.5f
}
fallbackSpeed = String.format("%.1f", speed).toFloat()
}
.setNegativeButton(R.string.cancel_label, null)
.show()
}
companion object {
const val TAG: String = "EditForwardSpeedDialog"
}
}

View File

@ -0,0 +1,45 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.EditTextDialogBinding
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.speedforwardSpeed
import android.app.Activity
import android.content.DialogInterface
import android.text.Editable
import android.text.InputType
import android.view.LayoutInflater
import androidx.media3.common.util.UnstableApi
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
@UnstableApi
class EditForwardSpeedDialog(activity: Activity) {
private val activityRef = WeakReference(activity)
fun show() {
val activity = activityRef.get() ?: return
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
binding.editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
binding.editText.text = Editable.Factory.getInstance().newEditable(speedforwardSpeed.toString())
MaterialAlertDialogBuilder(activity)
.setView(binding.root)
.setTitle(R.string.edit_fast_forward_speed)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
var speed = binding.editText.text.toString().toFloatOrNull() ?: 0.0f
when {
speed < 0.0f -> speed = 0.0f
speed > 10.0f -> speed = 10.0f
}
speedforwardSpeed = String.format("%.1f", speed).toFloat()
}
.setNegativeButton(R.string.cancel_label, null)
.show()
}
companion object {
const val TAG: String = "EditForwardSpeedDialog"
}
}

View File

@ -25,12 +25,12 @@ import java.util.concurrent.ExecutionException
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)) val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
binding.urlEditText.setText(feed.download_url) binding.editText.setText(feed.download_url)
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setView(binding.root) .setView(binding.root)
.setTitle(R.string.edit_url_menu) .setTitle(R.string.edit_url_menu)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> showConfirmAlertDialog(binding.urlEditText.text.toString()) } .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> showConfirmAlertDialog(binding.editText.text.toString()) }
.setNegativeButton(R.string.cancel_label, null) .setNegativeButton(R.string.cancel_label, null)
.show() .show()
} }

View File

@ -16,8 +16,7 @@ object FeedSortDialog {
dialog.setNegativeButton(android.R.string.cancel) { d: DialogInterface, _: Int -> d.dismiss() } dialog.setNegativeButton(android.R.string.cancel) { d: DialogInterface, _: Int -> d.dismiss() }
val selected = feedOrder val selected = feedOrder
val entryValues = val entryValues = listOf(*context.resources.getStringArray(R.array.nav_drawer_feed_order_values))
listOf(*context.resources.getStringArray(R.array.nav_drawer_feed_order_values))
val selectedIndex = entryValues.indexOf("" + selected) val selectedIndex = entryValues.indexOf("" + selected)
val items = context.resources.getStringArray(R.array.nav_drawer_feed_order_options) val items = context.resources.getStringArray(R.array.nav_drawer_feed_order_options)

View File

@ -37,12 +37,12 @@ class RenameItemDialog {
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)) val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
val title = if (feed != null) feed!!.title else drawerItem!!.title val title = if (feed != null) feed!!.title else drawerItem!!.title
binding.urlEditText.setText(title) binding.editText.setText(title)
val dialog = MaterialAlertDialogBuilder(activity) val dialog = MaterialAlertDialogBuilder(activity)
.setView(binding.root) .setView(binding.root)
.setTitle(if (feed != null) R.string.rename_feed_label else R.string.rename_tag_label) .setTitle(if (feed != null) R.string.rename_feed_label else R.string.rename_tag_label)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val newTitle = binding.urlEditText.text.toString() val newTitle = binding.editText.text.toString()
if (feed != null) { if (feed != null) {
feed!!.setCustomTitle(newTitle) feed!!.setCustomTitle(newTitle)
DBWriter.setFeedCustomTitle(feed!!) DBWriter.setFeedCustomTitle(feed!!)
@ -56,7 +56,7 @@ class RenameItemDialog {
// To prevent cancelling the dialog on button click // To prevent cancelling the dialog on button click
dialog.getButton(AlertDialog.BUTTON_NEUTRAL) dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
.setOnClickListener { binding.urlEditText.setText(title) } .setOnClickListener { binding.editText.setText(title) }
} }
private fun renameTag(title: String) { private fun renameTag(title: String) {

View File

@ -117,18 +117,18 @@ class AddFeedFragment : Fragment() {
val builder = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(R.string.add_podcast_by_url) builder.setTitle(R.string.add_podcast_by_url)
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater) val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
dialogBinding.urlEditText.setHint(R.string.add_podcast_by_url_hint) dialogBinding.editText.setHint(R.string.add_podcast_by_url_hint)
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData: ClipData? = clipboard.primaryClip val clipData: ClipData? = clipboard.primaryClip
if (clipData != null && clipData.itemCount > 0 && clipData.getItemAt(0).text != null) { if (clipData != null && clipData.itemCount > 0 && clipData.getItemAt(0).text != null) {
val clipboardContent: String = clipData.getItemAt(0).text.toString() val clipboardContent: String = clipData.getItemAt(0).text.toString()
if (clipboardContent.trim { it <= ' ' }.startsWith("http")) { if (clipboardContent.trim { it <= ' ' }.startsWith("http")) {
dialogBinding.urlEditText.setText(clipboardContent.trim { it <= ' ' }) dialogBinding.editText.setText(clipboardContent.trim { it <= ' ' })
} }
} }
builder.setView(dialogBinding.root) builder.setView(dialogBinding.root)
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> addUrl(dialogBinding.urlEditText.text.toString()) } builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> addUrl(dialogBinding.editText.text.toString()) }
builder.setNegativeButton(R.string.cancel_label, null) builder.setNegativeButton(R.string.cancel_label, null)
builder.show() builder.show()
} }

View File

@ -4,6 +4,7 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.playback.PlaybackController import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.event.* import ac.mdiq.podcini.playback.event.*
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
@ -83,6 +84,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private lateinit var butFF: ImageButton private lateinit var butFF: ImageButton
private lateinit var txtvFF: TextView private lateinit var txtvFF: TextView
private lateinit var butSkip: ImageButton private lateinit var butSkip: ImageButton
private lateinit var txtvSkip: TextView
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
private lateinit var playerFragment: View private lateinit var playerFragment: View
@ -140,6 +142,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
butFF = binding.butFF butFF = binding.butFF
txtvFF = binding.txtvFF txtvFF = binding.txtvFF
butSkip = binding.butSkip butSkip = binding.butSkip
txtvSkip = binding.txtvSkip
progressIndicator = binding.progLoading progressIndicator = binding.progLoading
cardViewSeek = binding.cardViewSeek cardViewSeek = binding.cardViewSeek
txtvSeek = binding.txtvSeek txtvSeek = binding.txtvSeek
@ -206,6 +209,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
controller?.init() controller?.init()
controller?.playPause() controller?.playPause()
} }
butPlay.setOnLongClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed)
}
true
}
butFF.setOnClickListener { butFF.setOnClickListener {
if (controller != null) { if (controller != null) {
val curr: Int = controller!!.position val curr: Int = controller!!.position
@ -215,11 +225,18 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
butFF.setOnLongClickListener { butFF.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF)
false true
} }
butSkip.setOnClickListener { butSkip.setOnClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) controller!!.speedForward(speedForward)
}
}
butSkip.setOnLongClickListener {
activity?.sendBroadcast( activity?.sendBroadcast(
MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
true
} }
} }
@ -323,7 +340,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
if (UserPreferences.speedforwardSpeed > 0.1f) {
txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
} else txtvSkip.visibility = View.GONE
} }
override fun onStop() { override fun onStop() {

View File

@ -69,6 +69,7 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private lateinit var butFF: ImageButton private lateinit var butFF: ImageButton
private lateinit var txtvFF: TextView private lateinit var txtvFF: TextView
private lateinit var butSkip: ImageButton private lateinit var butSkip: ImageButton
private lateinit var txtvSkip: TextView
private lateinit var txtvPosition: TextView private lateinit var txtvPosition: TextView
private lateinit var txtvLength: TextView private lateinit var txtvLength: TextView
@ -97,6 +98,7 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
butFF = binding.butFF butFF = binding.butFF
txtvFF = binding.txtvFF txtvFF = binding.txtvFF
butSkip = binding.butSkip butSkip = binding.butSkip
txtvSkip = binding.txtvSkip
sbPosition = binding.sbPosition sbPosition = binding.sbPosition
txtvPosition = binding.txtvPosition txtvPosition = binding.txtvPosition
txtvLength = binding.txtvLength txtvLength = binding.txtvLength
@ -123,7 +125,6 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
controller = setupPlaybackController() controller = setupPlaybackController()
controller!!.init() controller!!.init()
// loadMediaInfo()
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
return binding.root return binding.root
} }
@ -169,6 +170,13 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
controller?.init() controller?.init()
controller?.playPause() controller?.playPause()
} }
butPlay.setOnLongClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed)
}
true
}
butFF.setOnClickListener { butFF.setOnClickListener {
if (controller != null) { if (controller != null) {
val curr: Int = controller!!.position val curr: Int = controller!!.position
@ -178,11 +186,18 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
butFF.setOnLongClickListener { butFF.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.showSkipPreference(requireContext(),
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF)
false true
} }
butSkip.setOnClickListener { butSkip.setOnClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) controller!!.speedForward(speedForward)
}
}
butSkip.setOnLongClickListener {
activity?.sendBroadcast( activity?.sendBroadcast(
MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
true
} }
} }
@ -280,6 +295,9 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
super.onStart() super.onStart()
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
if (UserPreferences.speedforwardSpeed > 0.1f) {
txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
} else txtvSkip.visibility = View.GONE
val media = controller?.getMedia() ?: return val media = controller?.getMedia() ?: return
updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)))
} }

View File

@ -552,12 +552,12 @@ class OnlineFeedViewFragment : Fragment() {
builder.setTitle(R.string.edit_url_menu) builder.setTitle(R.string.edit_url_menu)
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater) val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
if (downloader != null) { if (downloader != null) {
dialogBinding.urlEditText.setText(downloader!!.downloadRequest.source) dialogBinding.editText.setText(downloader!!.downloadRequest.source)
} }
builder.setView(dialogBinding.root) builder.setView(dialogBinding.root)
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
setLoadingLayout() setLoadingLayout()
lookupUrlAndDownload(dialogBinding.urlEditText.text.toString()) lookupUrlAndDownload(dialogBinding.editText.text.toString())
} }
builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, _: Int -> dialog1.cancel() } builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, _: Int -> dialog1.cancel() }
builder.setOnCancelListener {} builder.setOnCancelListener {}

View File

@ -306,6 +306,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) {
// Sent when playback position is reset // Sent when playback position is reset
Log.d(TAG, "onUnreadItemsChanged() called with event = [$event]")
loadItems(false) loadItems(false)
refreshToolbarState() refreshToolbarState()
} }

View File

@ -5,13 +5,17 @@ import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UsageStatistics.doNotAskAgain import ac.mdiq.podcini.preferences.UsageStatistics.doNotAskAgain
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.dialog.EditFallbackSpeedDialog
import ac.mdiq.podcini.ui.dialog.EditForwardSpeedDialog
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.annotation.OptIn
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import androidx.media3.common.util.UnstableApi
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@ -30,7 +34,7 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.playback_pref) (activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.playback_pref)
} }
private fun setupPlaybackScreen() { @OptIn(UnstableApi::class) private fun setupPlaybackScreen() {
val activity: Activity? = activity val activity: Activity? = activity
findPreference<Preference>(PREF_PLAYBACK_SPEED_LAUNCHER)!!.onPreferenceClickListener = findPreference<Preference>(PREF_PLAYBACK_SPEED_LAUNCHER)!!.onPreferenceClickListener =
@ -43,6 +47,16 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null) SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true true
} }
findPreference<Preference>(PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER)!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
EditForwardSpeedDialog(requireActivity()).show()
true
}
findPreference<Preference>(PREF_PLAYBACK_FALLBACK_SPEED_LAUNCHER)!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
EditFallbackSpeedDialog(requireActivity()).show()
true
}
findPreference<Preference>(PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER)!!.onPreferenceClickListener = findPreference<Preference>(PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER)!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null) SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
@ -120,6 +134,8 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
companion object { companion object {
private const val PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher" private const val PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher"
private const val PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher" private const val PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher"
private const val PREF_PLAYBACK_FALLBACK_SPEED_LAUNCHER = "prefPlaybackFallbackSpeedLauncher"
private const val PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER = "prefPlaybackSpeedForwardLauncher"
private const val PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher" private const val PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"
private const val PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload" private const val PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload"
} }

View File

@ -1,3 +1,4 @@
package ac.mdiq.podcini.util.event package ac.mdiq.podcini.util.event
//TODO: need to specify ids
class FavoritesEvent class FavoritesEvent

View File

@ -3,6 +3,7 @@ package ac.mdiq.podcini.util.event
import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedItem
// TODO: this appears not being posted
class FeedItemEvent(@JvmField val items: List<FeedItem>) { class FeedItemEvent(@JvmField val items: List<FeedItem>) {
companion object { companion object {
fun updated(items: List<FeedItem>): FeedItemEvent { fun updated(items: List<FeedItem>): FeedItemEvent {

View File

@ -1,3 +1,4 @@
package ac.mdiq.podcini.util.event package ac.mdiq.podcini.util.event
//TODO: need to be optimized
class PlayerStatusEvent class PlayerStatusEvent

View File

@ -1,3 +1,4 @@
package ac.mdiq.podcini.util.event package ac.mdiq.podcini.util.event
// TODO: need to specify ids
class UnreadItemsUpdateEvent class UnreadItemsUpdateEvent

View File

@ -283,6 +283,20 @@
app:srcCompat="@drawable/ic_skip_48dp" app:srcCompat="@drawable/ic_skip_48dp"
tools:srcCompat="@drawable/ic_skip_48dp" /> tools:srcCompat="@drawable/ic_skip_48dp" />
<TextView
android:id="@+id/txtvSkip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butSkip"
android:layout_alignStart="@id/butSkip"
android:layout_alignLeft="@id/butSkip"
android:layout_alignEnd="@id/butSkip"
android:layout_alignRight="@id/butSkip"
android:clickable="false"
android:gravity="center"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@ -8,8 +8,8 @@
<EditText <EditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" android:inputType="text|numberDecimal"
android:ems="10" android:ems="10"
android:id="@+id/urlEditText" /> android:id="@+id/editText" />
</LinearLayout> </LinearLayout>

View File

@ -208,6 +208,20 @@
tools:srcCompat="@drawable/ic_skip_48dp" tools:srcCompat="@drawable/ic_skip_48dp"
app:tint="@color/medium_gray"/> app:tint="@color/medium_gray"/>
<TextView
android:id="@+id/txtvSkip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/butSkip"
android:layout_alignStart="@id/butSkip"
android:layout_alignLeft="@id/butSkip"
android:layout_alignEnd="@id/butSkip"
android:layout_alignRight="@id/butSkip"
android:clickable="false"
android:gravity="center"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@ -181,6 +181,7 @@
<item>@string/drawer_feed_order_unplayed_episodes</item> <item>@string/drawer_feed_order_unplayed_episodes</item>
<item>@string/drawer_feed_order_alphabetical</item> <item>@string/drawer_feed_order_alphabetical</item>
<item>@string/drawer_feed_order_last_update</item> <item>@string/drawer_feed_order_last_update</item>
<item>@string/drawer_feed_order_last_unread_update</item>
<item>@string/drawer_feed_order_most_played</item> <item>@string/drawer_feed_order_most_played</item>
</string-array> </string-array>
<string-array name="nav_drawer_feed_order_values"> <string-array name="nav_drawer_feed_order_values">
@ -188,6 +189,7 @@
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
<item>3</item> <item>3</item>
<item>4</item>
</string-array> </string-array>
<string-array name="nav_drawer_feed_counter_options"> <string-array name="nav_drawer_feed_counter_options">

View File

@ -69,10 +69,11 @@
<string name="drawer_open">Open menu</string> <string name="drawer_open">Open menu</string>
<string name="drawer_close">Close menu</string> <string name="drawer_close">Close menu</string>
<string name="drawer_preferences">Drawer preferences</string> <string name="drawer_preferences">Drawer preferences</string>
<string name="drawer_feed_order_unplayed_episodes">Sort by counter</string> <string name="drawer_feed_order_unplayed_episodes">Counter</string>
<string name="drawer_feed_order_alphabetical">Sort alphabetically</string> <string name="drawer_feed_order_alphabetical">Title</string>
<string name="drawer_feed_order_last_update">Sort by publication date</string> <string name="drawer_feed_order_last_update">Publication date</string>
<string name="drawer_feed_order_most_played">Sort by number of played episodes</string> <string name="drawer_feed_order_last_unread_update">Unread publication date</string>
<string name="drawer_feed_order_most_played">Number of played episodes</string>
<string name="drawer_feed_counter_unplayed">Number of unplayed episodes</string> <string name="drawer_feed_counter_unplayed">Number of unplayed episodes</string>
<string name="drawer_feed_counter_downloaded">Number of downloaded episodes</string> <string name="drawer_feed_counter_downloaded">Number of downloaded episodes</string>
@ -445,7 +446,7 @@
<string name="pref_tinted_theme_message">Adapt app colors based on the wallpaper</string> <string name="pref_tinted_theme_message">Adapt app colors based on the wallpaper</string>
<string name="pref_nav_drawer_items_title">Set navigation drawer items</string> <string name="pref_nav_drawer_items_title">Set navigation drawer items</string>
<string name="pref_nav_drawer_items_sum">Change which items appear in the navigation drawer</string> <string name="pref_nav_drawer_items_sum">Change which items appear in the navigation drawer</string>
<string name="pref_nav_drawer_feed_order_title">Set subscription order</string> <string name="pref_nav_drawer_feed_order_title">Set subscription order by</string>
<string name="pref_nav_drawer_feed_order_sum">Change the order of your subscriptions</string> <string name="pref_nav_drawer_feed_order_sum">Change the order of your subscriptions</string>
<string name="pref_nav_drawer_feed_counter_title">Set subscription counter</string> <string name="pref_nav_drawer_feed_counter_title">Set subscription counter</string>
<string name="pref_nav_drawer_feed_counter_sum">Change the information displayed by the subscription counter. Also affects the sorting of subscriptions if \'Subscription Order\' is set to \'Counter\'.</string> <string name="pref_nav_drawer_feed_counter_sum">Change the information displayed by the subscription counter. Also affects the sorting of subscriptions if \'Subscription Order\' is set to \'Counter\'.</string>
@ -475,6 +476,10 @@
<string name="pref_feed_skip_intro_toast">Skipped first %d seconds</string> <string name="pref_feed_skip_intro_toast">Skipped first %d seconds</string>
<string name="pref_playback_time_respects_speed_title">Adjust media info to playback speed</string> <string name="pref_playback_time_respects_speed_title">Adjust media info to playback speed</string>
<string name="pref_playback_time_respects_speed_sum">Displayed position and duration are adapted to playback speed</string> <string name="pref_playback_time_respects_speed_sum">Displayed position and duration are adapted to playback speed</string>
<string name="pref_fallback_speed">Fallback speed</string>
<string name="pref_speed_forward">Fast forward speed</string>
<string name="pref_speed_forward_sum">Customize the speed to speed forward when the skip button is clicked</string>
<string name="pref_fallback_speed_sum">Customize the speed when the play button is long pressed</string>
<string name="pref_fast_forward">Fast forward skip time</string> <string name="pref_fast_forward">Fast forward skip time</string>
<string name="pref_fast_forward_sum">Customize the number of seconds to jump forward when the fast forward button is clicked</string> <string name="pref_fast_forward_sum">Customize the number of seconds to jump forward when the fast forward button is clicked</string>
<string name="pref_rewind">Rewind skip time</string> <string name="pref_rewind">Rewind skip time</string>
@ -724,6 +729,7 @@
<string name="wait_icon" translatable="false">{fa-spinner}</string> <string name="wait_icon" translatable="false">{fa-spinner}</string>
<string name="edit_url_menu">Edit feed URL</string> <string name="edit_url_menu">Edit feed URL</string>
<string name="edit_url_confirmation_msg">Changing the RSS address can easily break the playback state and episode listings of the podcast. We do NOT recommend changing it and will NOT provide support if anything goes wrong. This cannot be undone. The broken subscription CANNOT be repaired by simply changing the address back. We suggest creating a backup before continuing.</string> <string name="edit_url_confirmation_msg">Changing the RSS address can easily break the playback state and episode listings of the podcast. We do NOT recommend changing it and will NOT provide support if anything goes wrong. This cannot be undone. The broken subscription CANNOT be repaired by simply changing the address back. We suggest creating a backup before continuing.</string>
<string name="edit_fast_forward_speed">Edit fast forward speed</string>
<!-- PodciniSP --> <!-- PodciniSP -->
<string name="sp_apps_importing_feeds_msg">Importing subscriptions from single-purpose apps&#8230;</string> <string name="sp_apps_importing_feeds_msg">Importing subscriptions from single-purpose apps&#8230;</string>

View File

@ -43,11 +43,19 @@
android:key="prefPlaybackSpeedLauncher" android:key="prefPlaybackSpeedLauncher"
android:summary="@string/pref_playback_speed_sum" android:summary="@string/pref_playback_speed_sum"
android:title="@string/playback_speed"/> android:title="@string/playback_speed"/>
<Preference
android:key="prefPlaybackFallbackSpeedLauncher"
android:summary="@string/pref_fallback_speed_sum"
android:title="@string/pref_fallback_speed"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="prefPlaybackTimeRespectsSpeed" android:key="prefPlaybackTimeRespectsSpeed"
android:summary="@string/pref_playback_time_respects_speed_sum" android:summary="@string/pref_playback_time_respects_speed_sum"
android:title="@string/pref_playback_time_respects_speed_title"/> android:title="@string/pref_playback_time_respects_speed_title"/>
<Preference
android:key="prefPlaybackSpeedForwardLauncher"
android:summary="@string/pref_speed_forward_sum"
android:title="@string/pref_speed_forward"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="prefStreamOverDownload" android:key="prefStreamOverDownload"

View File

@ -181,3 +181,21 @@
* online episodes list view goes back to the online feed view * online episodes list view goes back to the online feed view
* the original online feed view activity is stripped and now only preserved for receiving shared feed * the original online feed view activity is stripped and now only preserved for receiving shared feed
* externally shared feed opens in the online feed view fragment * externally shared feed opens in the online feed view fragment
## 4.5.0
* fixed bug of sort order not stable in feed item list
* in Subscriptions view added sorting of "Unread publication date"
* added preference "Fast Forward Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 10.0)
* added preference "Fallback Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 1.5)
* added new ways to manipulate play speed
* the "Skip to next episode" button
* long-press moves to the next episode
* by default, single tap does nothing
* if the user customize "Fast Forward Speed" to a value greater than 0.1, it behaves in the following way:
* single tap during play, the set speed is used to play the current audio
* single tap again, the original play speed resumes
* single tap not during play has no effect
* the Play button
* by default, it behaves the same as usual
* if the user customize "Fallback speed" to a value greater than 0.1, long-press the button during play enters the fallback mode and plays at the set fallback speed, single tap exits the fallback mode

View File

@ -0,0 +1,18 @@
Version 4.5.0 brings several changes:
* fixed bug of sort order not stable in feed item list
* in Subscriptions view added sorting of "Unread publication date"
* added preference "Fast Forward Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 10.0)
* added preference "Fallback Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 1.5)
* added new ways to manipulate play speed
* the "Skip to next episode" button
* long-press moves to the next episode
* by default, single tap does nothing
* if the user customize "Fast Forward Speed" to a value greater than 0.1, it behaves in the following way:
* single tap during play, the set speed is used to play the current audio
* single tap again, the original play speed resumes
* single tap not during play has no effect
* the Play button
* by default, it behaves the same as usual
* if the user customize "Fallback speed" to a value greater than 0.1, long-press the button during play enters the fallback mode and plays at the set fallback speed, single tap exits the fallback mode