6.5.6 commit

This commit is contained in:
Xilin Jia 2024-09-07 13:31:22 +01:00
parent c2977301f6
commit 25f811a8bd
25 changed files with 598 additions and 532 deletions

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020239
versionName "6.5.5"
versionCode 3020240
versionName "6.5.6"
applicationId "ac.mdiq.podcini.R"
def commit = ""
@ -142,7 +142,6 @@ android {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
resValue "string", "app_name", "Podcini.R"
resValue "string", "provider_authority", "ac.mdiq.podcini.R.provider"
// debuggable false
vcsInfo.include false
minifyEnabled true
shrinkResources true
@ -180,18 +179,11 @@ dependencies {
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
// implementation 'androidx.compose.material3:material3:1.2.0'
implementation 'androidx.compose.material:material:1.7.0'
// implementation 'androidx.compose.foundation:foundation:1.6.2'
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0'
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0'
// Optional - Add full set of material icons
// implementation 'androidx.compose.material:material-icons-extended'
// Optional - Add window size utils
// implementation 'androidx.compose.material3:material3-window-size-class'
implementation 'androidx.activity:activity-compose:1.9.2'
implementation 'androidx.window:window:1.3.0'
@ -204,7 +196,6 @@ dependencies {
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation "androidx.fragment:fragment-ktx:1.8.3"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
// implementation "androidx.media:media:1.7.0"
implementation "androidx.media3:media3-exoplayer:1.4.1"
implementation "androidx.media3:media3-ui:1.4.1"
implementation "androidx.media3:media3-datasource-okhttp:1.4.1"
@ -233,7 +224,6 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
implementation 'com.squareup.okio:okio:3.9.0'
// implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"

View File

@ -45,8 +45,8 @@
android:supportsRtl="true"
android:logo="@mipmap/ic_launcher"
android:resizeableActivity="true"
android:allowAudioPlaybackCapture="true">
<!-- android:networkSecurityConfig="@xml/network_security_config">-->
android:allowAudioPlaybackCapture="true"
android:networkSecurityConfig="@xml/network_security_config">
<service android:name=".playback.service.PlaybackService"
android:foregroundServiceType="mediaPlayback"

View File

@ -5,16 +5,11 @@ import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -191,28 +186,5 @@ abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
companion object {
private val TAG: String = ServiceStatusHandler::class.simpleName ?: "Anonymous"
/**
* Returns an intent which starts an audio- or videoplayer, depending on the
* type of media that is being played. If the playbackservice is not
* running, the type of the last played media will be looked up.
*/
@JvmStatic
fun getPlayerActivityIntent(context: Context): Intent {
val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting
else curState.curIsVideo
return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
else MainActivityStarter(context).withOpenPlayer().getIntent()
}
/**
* Same as [.getPlayerActivityIntent], but here the type of activity
* depends on the medaitype that is provided as an argument.
*/
@JvmStatic
fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent
else MainActivityStarter(context).withOpenPlayer().getIntent()
}
}
}

View File

@ -0,0 +1,19 @@
package ac.mdiq.podcini.playback.base
enum class VideoMode(val code: Int, val tag: String) {
NONE(0, "none"),
WINDOW_VIEW(1, "window mode"),
FULL_SCREEN_VIEW(2, "full screen"),
AUDIO_ONLY(3, "audio only");
companion object {
val videoModeTags = VideoMode.entries.map { it.tag }
fun fromCode(code: Int): VideoMode {
return enumValues<VideoMode>().firstOrNull { it.code == code } ?: NONE
}
fun fromTag(tag: String): VideoMode {
return enumValues<VideoMode>().firstOrNull { it.tag == tag } ?: NONE
}
}
}

View File

@ -12,12 +12,10 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@ -197,7 +195,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
val audioStream = audioStreamsList[audioIndex]
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
val aSource = DefaultMediaSourceFactory(context).createMediaSource(MediaItem.Builder().setTag(metadata).setUri(Uri.parse(audioStream.content)).build())
if (media.episode?.feed?.preferences?.playAudioOnly != true) {
if (media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
Logd(TAG, "setDataSource1 result: $streamInfo")
Logd(TAG, "setDataSource1 videoStreams: ${streamInfo.videoStreams.size} videoOnlyStreams: ${streamInfo.videoOnlyStreams.size} audioStreams: ${streamInfo.audioStreams.size}")
val videoStreamsList = getSortedStreamVideosList(streamInfo.videoStreams, streamInfo.videoOnlyStreams, true, true)

View File

@ -52,6 +52,8 @@ import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYIN
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.util.EventFlow
@ -1440,5 +1442,18 @@ class PlaybackService : MediaLibraryService() {
}
}
}
/**
* Returns an intent which starts an audio- or videoplayer, depending on the
* type of media that is being played or the medaitype that is provided as an argument.
* If the playbackservice is not running, the type of the last played media will be looked up.
*/
@JvmStatic
fun getPlayerActivityIntent(context: Context, mediaType_: MediaType? = null): Intent {
val mediaType = mediaType_ ?: currentMediaType
val showVideoPlayer = if (isRunning) mediaType == MediaType.VIDEO && !isCasting else curState.curIsVideo
return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
else MainActivityStarter(context).withOpenPlayer().getIntent()
}
}
}

View File

@ -23,6 +23,269 @@ import java.net.Proxy
object UserPreferences {
private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous"
// Experimental
const val EPISODE_CLEANUP_QUEUE: Int = -1
const val EPISODE_CLEANUP_NULL: Int = -2
const val EPISODE_CLEANUP_EXCEPT_FAVORITE: Int = -3
const val EPISODE_CLEANUP_DEFAULT: Int = 0
const val EPISODE_CACHE_SIZE_UNLIMITED: Int = -1
const val DEFAULT_PAGE_REMEMBER: String = "remember"
private lateinit var context: Context
lateinit var appPrefs: SharedPreferences
var theme: ThemePreference
get() = when (appPrefs.getString(Prefs.prefTheme.name, "system")) {
"0" -> ThemePreference.LIGHT
"1" -> ThemePreference.DARK
else -> ThemePreference.SYSTEM
}
set(theme) {
when (theme) {
ThemePreference.LIGHT -> appPrefs.edit().putString(Prefs.prefTheme.name, "0").apply()
ThemePreference.DARK -> appPrefs.edit().putString(Prefs.prefTheme.name, "1").apply()
else -> appPrefs.edit().putString(Prefs.prefTheme.name, "system").apply()
}
}
val isBlackTheme: Boolean
get() = appPrefs.getBoolean(Prefs.prefThemeBlack.name, false)
val isThemeColorTinted: Boolean
get() = Build.VERSION.SDK_INT >= 31 && appPrefs.getBoolean(Prefs.prefTintedColors.name, false)
var hiddenDrawerItems: List<String>
get() {
val hiddenItems = appPrefs.getString(Prefs.prefHiddenDrawerItems.name, "")
return hiddenItems?.split(",") ?: listOf()
}
set(items) {
val str = items.joinToString()
appPrefs.edit().putString(Prefs.prefHiddenDrawerItems.name, str).apply()
}
var fullNotificationButtons: List<Int>
get() {
val buttons = appPrefs.getString(Prefs.prefFullNotificationButtons.name, "${NOTIFICATION_BUTTON.SKIP.ordinal},${NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal}")?.split(",") ?: listOf()
val notificationButtons: MutableList<Int> = ArrayList()
for (button in buttons) {
notificationButtons.add(button.toInt())
}
return notificationButtons
}
set(items) {
val str = items.joinToString()
appPrefs.edit().putString(Prefs.prefFullNotificationButtons.name, str).apply()
}
val isAutoDelete: Boolean
get() = appPrefs.getBoolean(Prefs.prefAutoDelete.name, false)
val isAutoDeleteLocal: Boolean
get() = appPrefs.getBoolean(Prefs.prefAutoDeleteLocal.name, false)
val videoPlayMode: Int
get() {
try { return appPrefs.getString(Prefs.prefVideoPlaybackMode.name, "1")!!.toInt()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setVideoMode(1)
return 1
}
}
var videoPlaybackSpeed: Float
get() {
try { return appPrefs.getString(Prefs.prefVideoPlaybackSpeed.name, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
videoPlaybackSpeed = 1.0f
return 1.0f
}
}
set(speed) {
appPrefs.edit().putString(Prefs.prefVideoPlaybackSpeed.name, speed.toString()).apply()
}
var isSkipSilence: Boolean
get() = appPrefs.getBoolean(Prefs.prefSkipSilence.name, false)
set(skipSilence) {
appPrefs.edit().putBoolean(Prefs.prefSkipSilence.name, skipSilence).apply()
}
/**
* Returns the capacity of the episode cache. This method will return the
* negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
* 'unlimited'.
*/
val episodeCacheSize: Int
get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt()
@set:VisibleForTesting
var isEnableAutodownload: Boolean
get() = appPrefs.getBoolean(Prefs.prefEnableAutoDl.name, false)
set(enabled) {
appPrefs.edit().putBoolean(Prefs.prefEnableAutoDl.name, enabled).apply()
}
val isEnableAutodownloadOnBattery: Boolean
get() = appPrefs.getBoolean(Prefs.prefEnableAutoDownloadOnBattery.name, true)
var speedforwardSpeed: Float
get() {
try { return appPrefs.getString(Prefs.prefSpeedforwardSpeed.name, "0.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
speedforwardSpeed = 0.0f
return 0.0f
}
}
set(speed) {
appPrefs.edit().putString(Prefs.prefSpeedforwardSpeed.name, speed.toString()).apply()
}
var fallbackSpeed: Float
get() {
try { return appPrefs.getString(Prefs.prefFallbackSpeed.name, "0.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
fallbackSpeed = 0.0f
return 0.0f
}
}
set(speed) {
appPrefs.edit().putString(Prefs.prefFallbackSpeed.name, speed.toString()).apply()
}
var fastForwardSecs: Int
get() = appPrefs.getInt(Prefs.prefFastForwardSecs.name, 30)
set(secs) {
appPrefs.edit().putInt(Prefs.prefFastForwardSecs.name, secs).apply()
}
var rewindSecs: Int
get() = appPrefs.getInt(Prefs.prefRewindSecs.name, 10)
set(secs) {
appPrefs.edit().putInt(Prefs.prefRewindSecs.name, secs).apply()
}
var proxyConfig: ProxyConfig
get() {
val type = Proxy.Type.valueOf(appPrefs.getString(Prefs.prefProxyType.name, Proxy.Type.DIRECT.name)!!)
val host = appPrefs.getString(Prefs.prefProxyHost.name, null)
val port = appPrefs.getInt(Prefs.prefProxyPort.name, 0)
val username = appPrefs.getString(Prefs.prefProxyUser.name, null)
val password = appPrefs.getString(Prefs.prefProxyPassword.name, null)
return ProxyConfig(type, host, port, username, password)
}
set(config) {
val editor = appPrefs.edit()
editor.putString(Prefs.prefProxyType.name, config.type.name)
if (config.host.isNullOrEmpty()) editor.remove(Prefs.prefProxyHost.name)
else editor.putString(Prefs.prefProxyHost.name, config.host)
if (config.port <= 0 || config.port > 65535) editor.remove(Prefs.prefProxyPort.name)
else editor.putInt(Prefs.prefProxyPort.name, config.port)
if (config.username.isNullOrEmpty()) editor.remove(Prefs.prefProxyUser.name)
else editor.putString(Prefs.prefProxyUser.name, config.username)
if (config.password.isNullOrEmpty()) editor.remove(Prefs.prefProxyPassword.name)
else editor.putString(Prefs.prefProxyPassword.name, config.password)
editor.apply()
}
var defaultPage: String?
get() = appPrefs.getString(Prefs.prefDefaultPage.name, "SubscriptionsFragment")
set(defaultPage) {
appPrefs.edit().putString(Prefs.prefDefaultPage.name, defaultPage).apply()
}
var isStreamOverDownload: Boolean
get() = appPrefs.getBoolean(Prefs.prefStreamOverDownload.name, false)
set(stream) {
appPrefs.edit().putBoolean(Prefs.prefStreamOverDownload.name, stream).apply()
}
/**
* Sets up the UserPreferences class.
* @throws IllegalArgumentException if context is null
*/
fun init(context: Context) {
Logd(TAG, "Creating new instance of UserPreferences")
UserPreferences.context = context.applicationContext
FilesUtils.context = context.applicationContext
appPrefs = PreferenceManager.getDefaultSharedPreferences(context)
createNoMediaFile()
}
/**
* Helper function to return whether the specified button should be shown on full
* notifications.
* @param buttonId Either NOTIFICATION_BUTTON_REWIND, NOTIFICATION_BUTTON_FAST_FORWARD,
* NOTIFICATION_BUTTON.SKIP.ordinal, NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal
* or NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal.
* @return `true` if button should be shown, `false` otherwise
*/
private fun showButtonOnFullNotification(buttonId: Int): Boolean {
return fullNotificationButtons.contains(buttonId)
}
// only used in test
fun showSkipOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON.SKIP.ordinal)
}
// only used in test
fun showNextChapterOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal)
}
// only used in test
fun showPlaybackSpeedOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal)
}
/**
* @return `true` if we should show remaining time or the duration
*/
fun shouldShowRemainingTime(): Boolean {
return appPrefs.getBoolean(Prefs.showTimeLeft.name, false)
}
/**
* Sets the preference for whether we show the remain time, if not show the duration. This will
* send out events so the current playing screen, queue and the episode list would refresh
* @return `true` if we should show remaining time or the duration
*/
fun setShowRemainTimeSetting(showRemain: Boolean?) {
appPrefs.edit().putBoolean(Prefs.showTimeLeft.name, showRemain!!).apply()
}
// only used in test
fun shouldPauseForFocusLoss(): Boolean {
return appPrefs.getBoolean(Prefs.prefPauseForFocusLoss.name, true)
}
fun backButtonOpensDrawer(): Boolean {
return appPrefs.getBoolean(Prefs.prefBackButtonOpensDrawer.name, false)
}
fun timeRespectsSpeed(): Boolean {
return appPrefs.getBoolean(Prefs.prefPlaybackTimeRespectsSpeed.name, false)
}
fun setPlaybackSpeed(speed: Float) {
appPrefs.edit().putString(Prefs.prefPlaybackSpeed.name, speed.toString()).apply()
}
fun setVideoMode(mode: Int) {
appPrefs.edit().putString(Prefs.prefVideoPlaybackMode.name, mode.toString()).apply()
}
@Suppress("EnumEntryName")
enum class Prefs {
prefOPMLBackup,
@ -109,12 +372,6 @@ object UserPreferences {
prefVideoPlaybackMode,
}
// Experimental
const val EPISODE_CLEANUP_QUEUE: Int = -1
const val EPISODE_CLEANUP_NULL: Int = -2
const val EPISODE_CLEANUP_EXCEPT_FAVORITE: Int = -3
const val EPISODE_CLEANUP_DEFAULT: Int = 0
// Constants
enum class NOTIFICATION_BUTTON {
REWIND,
@ -124,274 +381,6 @@ object UserPreferences {
PLAYBACK_SPEED,
}
const val EPISODE_CACHE_SIZE_UNLIMITED: Int = -1
const val DEFAULT_PAGE_REMEMBER: String = "remember"
private lateinit var context: Context
lateinit var appPrefs: SharedPreferences
var theme: ThemePreference
get() = when (appPrefs.getString(Prefs.prefTheme.name, "system")) {
"0" -> ThemePreference.LIGHT
"1" -> ThemePreference.DARK
else -> ThemePreference.SYSTEM
}
set(theme) {
when (theme) {
ThemePreference.LIGHT -> appPrefs.edit().putString(Prefs.prefTheme.name, "0").apply()
ThemePreference.DARK -> appPrefs.edit().putString(Prefs.prefTheme.name, "1").apply()
else -> appPrefs.edit().putString(Prefs.prefTheme.name, "system").apply()
}
}
val isBlackTheme: Boolean
get() = appPrefs.getBoolean(Prefs.prefThemeBlack.name, false)
val isThemeColorTinted: Boolean
get() = Build.VERSION.SDK_INT >= 31 && appPrefs.getBoolean(Prefs.prefTintedColors.name, false)
var hiddenDrawerItems: List<String>
get() {
val hiddenItems = appPrefs.getString(Prefs.prefHiddenDrawerItems.name, "")
return hiddenItems?.split(",") ?: listOf()
}
set(items) {
val str = items.joinToString()
appPrefs.edit()
.putString(Prefs.prefHiddenDrawerItems.name, str)
.apply()
}
var fullNotificationButtons: List<Int>
get() {
val buttons = appPrefs.getString(Prefs.prefFullNotificationButtons.name, "${NOTIFICATION_BUTTON.SKIP.ordinal},${NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal}")?.split(",") ?: listOf()
val notificationButtons: MutableList<Int> = ArrayList()
for (button in buttons) {
notificationButtons.add(button.toInt())
}
return notificationButtons
}
set(items) {
val str = items.joinToString()
appPrefs.edit()
.putString(Prefs.prefFullNotificationButtons.name, str)
.apply()
}
val isAutoDelete: Boolean
get() = appPrefs.getBoolean(Prefs.prefAutoDelete.name, false)
val isAutoDeleteLocal: Boolean
get() = appPrefs.getBoolean(Prefs.prefAutoDeleteLocal.name, false)
val videoPlayMode: Int
get() {
try {
return appPrefs.getString(Prefs.prefVideoPlaybackMode.name, "1")!!.toInt()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setVideoMode(1)
return 1
}
}
var videoPlaybackSpeed: Float
get() {
try {
return appPrefs.getString(Prefs.prefVideoPlaybackSpeed.name, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
videoPlaybackSpeed = 1.0f
return 1.0f
}
}
set(speed) {
appPrefs.edit()
.putString(Prefs.prefVideoPlaybackSpeed.name, speed.toString())
.apply()
}
var isSkipSilence: Boolean
get() = appPrefs.getBoolean(Prefs.prefSkipSilence.name, false)
set(skipSilence) {
appPrefs.edit().putBoolean(Prefs.prefSkipSilence.name, skipSilence).apply()
}
/**
* Returns the capacity of the episode cache. This method will return the
* negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
* 'unlimited'.
*/
val episodeCacheSize: Int
get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt()
@set:VisibleForTesting
var isEnableAutodownload: Boolean
get() = appPrefs.getBoolean(Prefs.prefEnableAutoDl.name, false)
set(enabled) {
appPrefs.edit().putBoolean(Prefs.prefEnableAutoDl.name, enabled).apply()
}
val isEnableAutodownloadOnBattery: Boolean
get() = appPrefs.getBoolean(Prefs.prefEnableAutoDownloadOnBattery.name, true)
var speedforwardSpeed: Float
get() {
try {
return appPrefs.getString(Prefs.prefSpeedforwardSpeed.name, "0.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
speedforwardSpeed = 0.0f
return 0.0f
}
}
set(speed) {
appPrefs.edit().putString(Prefs.prefSpeedforwardSpeed.name, speed.toString()).apply()
}
var fallbackSpeed: Float
get() {
try {
return appPrefs.getString(Prefs.prefFallbackSpeed.name, "0.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
fallbackSpeed = 0.0f
return 0.0f
}
}
set(speed) {
appPrefs.edit().putString(Prefs.prefFallbackSpeed.name, speed.toString()).apply()
}
var fastForwardSecs: Int
get() = appPrefs.getInt(Prefs.prefFastForwardSecs.name, 30)
set(secs) {
appPrefs.edit().putInt(Prefs.prefFastForwardSecs.name, secs).apply()
}
var rewindSecs: Int
get() = appPrefs.getInt(Prefs.prefRewindSecs.name, 10)
set(secs) {
appPrefs.edit().putInt(Prefs.prefRewindSecs.name, secs).apply()
}
var proxyConfig: ProxyConfig
get() {
val type = Proxy.Type.valueOf(appPrefs.getString(Prefs.prefProxyType.name, Proxy.Type.DIRECT.name)!!)
val host = appPrefs.getString(Prefs.prefProxyHost.name, null)
val port = appPrefs.getInt(Prefs.prefProxyPort.name, 0)
val username = appPrefs.getString(Prefs.prefProxyUser.name, null)
val password = appPrefs.getString(Prefs.prefProxyPassword.name, null)
return ProxyConfig(type, host, port, username, password)
}
set(config) {
val editor = appPrefs.edit()
editor.putString(Prefs.prefProxyType.name, config.type.name)
if (config.host.isNullOrEmpty()) editor.remove(Prefs.prefProxyHost.name)
else editor.putString(Prefs.prefProxyHost.name, config.host)
if (config.port <= 0 || config.port > 65535) editor.remove(Prefs.prefProxyPort.name)
else editor.putInt(Prefs.prefProxyPort.name, config.port)
if (config.username.isNullOrEmpty()) editor.remove(Prefs.prefProxyUser.name)
else editor.putString(Prefs.prefProxyUser.name, config.username)
if (config.password.isNullOrEmpty()) editor.remove(Prefs.prefProxyPassword.name)
else editor.putString(Prefs.prefProxyPassword.name, config.password)
editor.apply()
}
var defaultPage: String?
get() = appPrefs.getString(Prefs.prefDefaultPage.name, "SubscriptionsFragment")
set(defaultPage) {
appPrefs.edit().putString(Prefs.prefDefaultPage.name, defaultPage).apply()
}
var isStreamOverDownload: Boolean
get() = appPrefs.getBoolean(Prefs.prefStreamOverDownload.name, false)
set(stream) {
appPrefs.edit().putBoolean(Prefs.prefStreamOverDownload.name, stream).apply()
}
/**
* Sets up the UserPreferences class.
* @throws IllegalArgumentException if context is null
*/
fun init(context: Context) {
Logd(TAG, "Creating new instance of UserPreferences")
UserPreferences.context = context.applicationContext
FilesUtils.context = context.applicationContext
appPrefs = PreferenceManager.getDefaultSharedPreferences(context)
createNoMediaFile()
}
/**
* Helper function to return whether the specified button should be shown on full
* notifications.
* @param buttonId Either NOTIFICATION_BUTTON_REWIND, NOTIFICATION_BUTTON_FAST_FORWARD,
* NOTIFICATION_BUTTON.SKIP.ordinal, NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal
* or NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal.
* @return `true` if button should be shown, `false` otherwise
*/
private fun showButtonOnFullNotification(buttonId: Int): Boolean {
return fullNotificationButtons.contains(buttonId)
}
// only used in test
fun showSkipOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON.SKIP.ordinal)
}
// only used in test
fun showNextChapterOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal)
}
// only used in test
fun showPlaybackSpeedOnFullNotification(): Boolean {
return showButtonOnFullNotification(NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal)
}
/**
* @return `true` if we should show remaining time or the duration
*/
fun shouldShowRemainingTime(): Boolean {
return appPrefs.getBoolean(Prefs.showTimeLeft.name, false)
}
/**
* Sets the preference for whether we show the remain time, if not show the duration. This will
* send out events so the current playing screen, queue and the episode list would refresh
* @return `true` if we should show remaining time or the duration
*/
fun setShowRemainTimeSetting(showRemain: Boolean?) {
appPrefs.edit().putBoolean(Prefs.showTimeLeft.name, showRemain!!).apply()
}
// only used in test
fun shouldPauseForFocusLoss(): Boolean {
return appPrefs.getBoolean(Prefs.prefPauseForFocusLoss.name, true)
}
fun backButtonOpensDrawer(): Boolean {
return appPrefs.getBoolean(Prefs.prefBackButtonOpensDrawer.name, false)
}
fun timeRespectsSpeed(): Boolean {
return appPrefs.getBoolean(Prefs.prefPlaybackTimeRespectsSpeed.name, false)
}
fun setPlaybackSpeed(speed: Float) {
appPrefs.edit().putString(Prefs.prefPlaybackSpeed.name, speed.toString()).apply()
}
fun setVideoMode(mode: Int) {
appPrefs.edit().putString(Prefs.prefVideoPlaybackMode.name, mode.toString()).apply()
}
enum class ThemePreference {
LIGHT, DARK, BLACK, SYSTEM
}

View File

@ -190,11 +190,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
val fileUri = FileProvider.getUriForFile(context!!.applicationContext, context.getString(R.string.provider_authority), output!!)
showExportSuccessSnackbar(fileUri, exportType.contentType)
}
} catch (e: Exception) {
showTransportErrorDialog(e)
} finally {
progressDialog!!.dismiss()
}
} catch (e: Exception) { showTransportErrorDialog(e)
} finally { progressDialog!!.dismiss() }
}
} else {
lifecycleScope.launch(Dispatchers.IO) {
@ -204,18 +201,15 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
withContext(Dispatchers.Main) {
showExportSuccessSnackbar(output.uri, exportType.contentType)
}
} catch (e: Exception) {
showTransportErrorDialog(e)
} finally {
progressDialog!!.dismiss()
}
} catch (e: Exception) { showTransportErrorDialog(e)
} finally { progressDialog!!.dismiss() }
}
}
}
private fun exportPreferences() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_DEFAULT)
backupPreferencesLauncher.launch(intent)
}
@ -240,7 +234,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun exportMediaFiles() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_DEFAULT)
backupMediaFilesLauncher.launch(intent)
}
@ -277,6 +271,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
builder.setNegativeButton(R.string.no, null)
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setType("*/*")
intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream"))
intent.addCategory(Intent.CATEGORY_OPENABLE)
@ -321,6 +316,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
builder.setNegativeButton(R.string.no, null)
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setType("*/*")
intent.addCategory(Intent.CATEGORY_OPENABLE)
restoreProgressLauncher.launch(intent)
@ -331,25 +327,33 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun chooseProgressExportPathResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
exportWithWriter(EpisodesProgressWriter(), uri, Export.PROGRESS)
}
private fun chooseOpmlExportPathResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
exportWithWriter(OpmlWriter(), uri, Export.OPML)
}
private fun chooseHtmlExportPathResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
exportWithWriter(HtmlWriter(), uri, Export.HTML)
}
private fun chooseFavoritesExportPathResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
exportWithWriter(FavoritesWriter(), uri, Export.FAVORITES)
}
@ -357,6 +361,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data
uri?.let {
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isJsonFile(uri)) {
progressDialog!!.show()
lifecycleScope.launch {
@ -371,9 +377,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
} else {
val context = requireContext()
@ -392,6 +396,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
uri?.let {
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isRealmFile(uri)) {
progressDialog!!.show()
lifecycleScope.launch {
@ -403,9 +409,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
} else {
val context = requireContext()
@ -433,6 +437,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun restorePreferencesResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isPrefDir(uri)) {
progressDialog!!.show()
lifecycleScope.launch {
@ -444,9 +450,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
} else {
val context = requireContext()
@ -458,6 +462,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun restoreMediaFilesResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isMediaFilesDir(uri)) {
progressDialog!!.show()
lifecycleScope.launch {
@ -469,9 +475,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showImportSuccessDialog()
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
} else {
val context = requireContext()
@ -483,6 +487,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun exportMediaFilesResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
progressDialog!!.show()
lifecycleScope.launch {
try {
@ -493,9 +499,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(uri, null)
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
}
@ -511,9 +515,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(uri, "application/x-sqlite3")
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
}
@ -537,9 +539,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
try {
result.launch(intentPickAction)
return
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "No activity found. Should never happen...")
}
} catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") }
// If we are using a SDK lower than API 21 or the implicit intent failed
// fallback to the legacy export process
@ -578,8 +578,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
Logd(TAG, "feeds_: ${feeds_.size}")
exportWriter.writeDocument(feeds_, writer, context)
output
} catch (e: IOException) {
throw e
} catch (e: IOException) { throw e
} finally {
if (writer != null) try { writer.close() } catch (e: IOException) { throw e }
if (outputStream != null) try { outputStream.close() } catch (e: IOException) { throw e }
@ -610,9 +609,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
} catch (e: IOException) {
Log.e(TAG, "Error during file export", e)
null // return null in case of error
} finally {
writer?.close()
}
} finally { writer?.close() }
}
}
companion object {
@ -637,9 +634,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
val destFile = exportSubDir.createFile("text/xml", file.name)
if (destFile != null) copyFile(file, destFile, context)
}
} else {
Log.e("Error", "shared_prefs directory not found")
}
} else Log.e("Error", "shared_prefs directory not found")
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
throw e
@ -730,8 +725,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
object MediaFilesTransporter {
private val TAG: String = MediaFilesTransporter::class.simpleName ?: "Anonymous"
var feed: Feed? = null
val nameFeedMap: MutableMap<String, Feed> = mutableMapOf()
val nameEpisodeMap: MutableMap<String, Episode> = mutableMapOf()
private val nameFeedMap: MutableMap<String, Feed> = mutableMapOf()
private val nameEpisodeMap: MutableMap<String, Episode> = mutableMapOf()
@Throws(IOException::class)
fun exportToDocument(uri: Uri, context: Context) {
try {
@ -913,9 +908,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
throw e
} finally {
IOUtils.closeQuietly(inputStream)
}
} finally { IOUtils.closeQuietly(inputStream) }
}
}
@ -1124,6 +1117,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
}
@Suppress("EnumEntryName")
private enum class IExport {
prefOpmlExport,
prefOpmlImport,

View File

@ -117,15 +117,15 @@ object AutoDownloads {
if (allowedDLCount > 0) {
var queryString = "feedId == ${f.id} AND isAutoDownloadEnabled == true AND media != nil AND media.downloaded == false"
when (f.preferences?.autoDLPolicy) {
FeedPreferences.AutoDLPolicy.ONLY_NEW -> {
FeedPreferences.AutoDownloadPolicy.ONLY_NEW -> {
queryString += " AND playState == -1 SORT(pubDate DESC) LIMIT(${3*allowedDLCount})"
episodes = realm.query(Episode::class).query(queryString).find().toMutableList()
}
FeedPreferences.AutoDLPolicy.NEWER -> {
FeedPreferences.AutoDownloadPolicy.NEWER -> {
queryString += " AND playState != 1 SORT(pubDate DESC) LIMIT(${3*allowedDLCount})"
episodes = realm.query(Episode::class).query(queryString).find().toMutableList()
}
FeedPreferences.AutoDLPolicy.OLDER -> {
FeedPreferences.AutoDownloadPolicy.OLDER -> {
queryString += " AND playState != 1 SORT(pubDate ASC) LIMIT(${3*allowedDLCount})"
episodes = realm.query(Episode::class).query(queryString).find().toMutableList()
}

View File

@ -211,10 +211,11 @@ object Feeds {
Logd(TAG, "New feed has a higher page number.")
savedFeed.nextPageLink = newFeed.nextPageLink
}
if (savedFeed.preferences != null && savedFeed.preferences!!.compareWithOther(newFeed.preferences)) {
Logd(TAG, "Feed has updated preferences. Updating old feed's preferences")
savedFeed.preferences!!.updateFromOther(newFeed.preferences)
}
// appears not useful
// if (savedFeed.preferences != null && savedFeed.preferences!!.compareWithOther(newFeed.preferences)) {
// Logd(TAG, "Feed has updated preferences. Updating old feed's preferences")
// savedFeed.preferences!!.updateFromOther(newFeed.preferences)
// }
val priorMostRecent = savedFeed.mostRecentItem
val priorMostRecentDate: Date? = priorMostRecent?.getPubDate()
var idLong = Feed.newId()

View File

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

View File

@ -243,7 +243,6 @@ class Feed : RealmObject {
if (imageUrl == null || imageUrl != other.imageUrl) return true
}
if (eigenTitle != other.eigenTitle) return true
if (other.identifier != null) {
if (identifier == null || identifier != other.identifier) return true
}
@ -263,9 +262,7 @@ class Feed : RealmObject {
if (paymentLinks.isEmpty() || paymentLinks != other.paymentLinks) return true
}
if (other.isPaged && !this.isPaged) return true
if (other.nextPageLink != this.nextPageLink) return true
return false
}

View File

@ -2,9 +2,9 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger
import androidx.compose.runtime.mutableStateOf
import io.realm.kotlin.ext.realmSetOf
import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.RealmSet
@ -26,7 +26,15 @@ class FeedPreferences : EmbeddedRealmObject {
var username: String? = null
var password: String? = null
var playAudioOnly: Boolean = false
// var playAudioOnly: Boolean = false
@Ignore
var videoModePolicy: VideoMode = VideoMode.NONE
get() = VideoMode.fromCode(videoMode)
set(value) {
field = value
videoMode = field.code
}
var videoMode: Int = 0
var playSpeed: Float = SPEED_USE_GLOBAL
@ -118,22 +126,57 @@ class FeedPreferences : EmbeddedRealmObject {
var countingPlayed: Boolean = true
@Ignore
var autoDLPolicy: AutoDLPolicy = AutoDLPolicy.ONLY_NEW
get() = AutoDLPolicy.fromCode(autoDLPolicyCode)
var autoDLPolicy: AutoDownloadPolicy = AutoDownloadPolicy.ONLY_NEW
get() = AutoDownloadPolicy.fromCode(autoDLPolicyCode)
set(value) {
field = value
autoDLPolicyCode = value.code
}
var autoDLPolicyCode: Int = 0
enum class AutoDLPolicy(val code: Int, val resId: Int) {
constructor() {}
constructor(feedID: Long, autoDownload: Boolean, autoDeleteAction: AutoDeleteAction,
volumeAdaptionSetting: VolumeAdaptionSetting?, username: String?, password: String?) {
this.feedID = feedID
this.autoDownload = autoDownload
this.autoDeleteAction = autoDeleteAction
if (volumeAdaptionSetting != null) this.volumeAdaptionSetting = volumeAdaptionSetting
this.username = username
this.password = password
this.autoDelete = autoDeleteAction.code
this.volumeAdaption = volumeAdaptionSetting?.toInteger() ?: 0
}
// These appear not needed
/**
* Compare another FeedPreferences with this one. .
* @return True if the two objects are different.
*/
// fun compareWithOther(other: FeedPreferences?): Boolean {
// if (other == null) return true
// if (username != other.username) return true
// if (password != other.password) return true
// return false
// }
/**
* Update this FeedPreferences object from another one.
*/
// fun updateFromOther(other: FeedPreferences?) {
// if (other == null) return
// this.username = other.username
// this.password = other.password
// }
enum class AutoDownloadPolicy(val code: Int, val resId: Int) {
ONLY_NEW(0, R.string.feed_auto_download_new),
NEWER(1, R.string.feed_auto_download_newer),
OLDER(2, R.string.feed_auto_download_older);
companion object {
fun fromCode(code: Int): AutoDLPolicy {
return enumValues<AutoDLPolicy>().firstOrNull { it.code == code } ?: ONLY_NEW
fun fromCode(code: Int): AutoDownloadPolicy {
return enumValues<AutoDownloadPolicy>().firstOrNull { it.code == code } ?: ONLY_NEW
}
}
}
@ -153,48 +196,11 @@ class FeedPreferences : EmbeddedRealmObject {
}
}
constructor() {}
constructor(feedID: Long, autoDownload: Boolean, autoDeleteAction: AutoDeleteAction,
volumeAdaptionSetting: VolumeAdaptionSetting?, username: String?, password: String?) {
this.feedID = feedID
this.autoDownload = autoDownload
this.autoDeleteAction = autoDeleteAction
if (volumeAdaptionSetting != null) this.volumeAdaptionSetting = volumeAdaptionSetting
this.username = username
this.password = password
this.autoDelete = autoDeleteAction.code
this.volumeAdaption = volumeAdaptionSetting?.toInteger() ?: 0
}
/**
* Compare another FeedPreferences with this one. The feedID, autoDownload and AutoDeleteAction attribute are excluded from the
* comparison.
* @return True if the two objects are different.
*/
fun compareWithOther(other: FeedPreferences?): Boolean {
if (other == null) return true
if (username != other.username) return true
if (password != other.password) return true
return false
}
/**
* Update this FeedPreferences object from another one. The feedID, autoDownload and AutoDeleteAction attributes are excluded
* from the update.
*/
fun updateFromOther(other: FeedPreferences?) {
if (other == null) return
this.username = other.username
this.password = other.password
}
companion object {
const val SPEED_USE_GLOBAL: Float = -1f
const val TAG_ROOT: String = "#root"
const val TAG_SEPARATOR: String = "\u001e"
val FeedAutoDeleteOptions = AutoDeleteAction.values().map { it.tag }
val FeedAutoDeleteOptions = AutoDeleteAction.entries.map { it.tag }
}
}

View File

@ -2,10 +2,12 @@ package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
import android.content.Context
import android.view.View
import android.widget.ImageView
@ -32,6 +34,13 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
icon.setImageResource(getDrawable())
}
protected fun playVideo(context: Context, media: Playable) {
if (item.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
&& videoPlayMode != VideoMode.AUDIO_ONLY.code && videoMode != VideoMode.AUDIO_ONLY
&& media.getMediaType() == MediaType.VIDEO)
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
}
@UnstableApi companion object {
fun forItem(episode: Episode): EpisodeActionButton {
val media = episode.media ?: return TTSActionButton(episode)

View File

@ -1,21 +1,16 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.util.Log
import android.widget.Toast
@ -51,9 +46,11 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
EventFlow.postEvent(FlowEvent.PlayEvent(item))
}
if (item.feed?.preferences?.playAudioOnly != true && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY
&& media.getMediaType() == MediaType.VIDEO)
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
// if (item.feed?.preferences?.videoModePolicy != FeedPreferences.VideomodePolicy.AUDIO_ONLY
// && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY
// && media.getMediaType() == MediaType.VIDEO)
// context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
playVideo(context, media)
}
/**

View File

@ -1,9 +1,9 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType

View File

@ -2,18 +2,13 @@ package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UsageStatistics.logAction
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.RemoteMedia
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.content.Context
@ -47,8 +42,11 @@ class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
.start()
EventFlow.postEvent(FlowEvent.PlayEvent(item))
if (item.feed?.preferences?.playAudioOnly != true && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY
&& media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
// if (item.feed?.preferences?.videoModePolicy != FeedPreferences.VideomodePolicy.AUDIO_ONLY
// && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY
// && media.getMediaType() == MediaType.VIDEO)
// context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
playVideo(context, media)
}
class StreamingConfirmationDialog(private val context: Context, private val playable: Playable) {

View File

@ -5,14 +5,15 @@ import ac.mdiq.podcini.databinding.AudioControlsBinding
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
@ -36,17 +37,16 @@ import ac.mdiq.podcini.ui.fragment.ChaptersFragment
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.app.Activity
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.PixelFormat
import android.graphics.drawable.ColorDrawable
import android.media.AudioManager
@ -95,18 +95,25 @@ class VideoplayerActivity : CastEnabledActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
videoMode = (intent.getSerializableExtra(VIDEO_MODE) as? VideoMode) ?: VideoMode.None
if (videoMode == VideoMode.None) {
videoMode = VideoMode.entries.toTypedArray().getOrElse(videoPlayMode) { VideoMode.WINDOW_VIEW }
if (videoMode == VideoMode.AUDIO_ONLY) {
switchToAudioOnly = true
finish()
}
if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) {
Logd(TAG, "videoMode not selected, use window mode")
videoMode = VideoMode.WINDOW_VIEW
}
var vmCode = 0
if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia
val vPol = media_.episode?.feed?.preferences?.videoModePolicy
if (vPol != null && vPol != VideoMode.NONE) vmCode = vPol.code
}
Logd(TAG, "onCreate vmCode: $vmCode")
if (vmCode == 0) vmCode = videoPlayMode
Logd(TAG, "onCreate vmCode: $vmCode")
videoMode = VideoMode.entries.toTypedArray().getOrElse(vmCode) { VideoMode.WINDOW_VIEW }
if (videoMode == VideoMode.AUDIO_ONLY) {
switchToAudioOnly = true
finish()
}
if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) {
Logd(TAG, "videoMode not selected, use window mode")
videoMode = VideoMode.WINDOW_VIEW
}
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
setForVideoMode()
super.onCreate(savedInstanceState)
@ -393,13 +400,6 @@ class VideoplayerActivity : CastEnabledActivity() {
return super.onKeyUp(keyCode, event)
}
enum class VideoMode(val mode: Int) {
None(0),
WINDOW_VIEW(1),
FULL_SCREEN_VIEW(2),
AUDIO_ONLY(3)
}
class PlaybackControlsDialog : DialogFragment() {
private lateinit var dialog: AlertDialog
private var _binding: AudioControlsBinding? = null
@ -533,7 +533,7 @@ class VideoplayerActivity : CastEnabledActivity() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView")
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(activity))
_binding = VideoEpisodeFragmentBinding.inflate(inflater)
root = binding.root
statusHandler = newStatusHandler()
statusHandler!!.init()
@ -646,17 +646,19 @@ class VideoplayerActivity : CastEnabledActivity() {
private fun setupVideoAspectRatio() {
if (videoSurfaceCreated) {
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity as Activity)
val videoWidth = if (videoMode == VideoMode.FULL_SCREEN_VIEW) max(windowMetrics.bounds.width(), windowMetrics.bounds.height())
else min(windowMetrics.bounds.width(), windowMetrics.bounds.height())
var videoHeight = 0
val videoWidth = when (videoMode) {
VideoMode.FULL_SCREEN_VIEW -> max(windowMetrics.bounds.width(), windowMetrics.bounds.height())
VideoMode.WINDOW_VIEW -> min(windowMetrics.bounds.width(), windowMetrics.bounds.height())
else -> min(windowMetrics.bounds.width(), windowMetrics.bounds.height())
}
val videoHeight: Int
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
Logd(TAG, "setupVideoAspectRatio Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
Logd(TAG, "setupVideoAspectRatio video width: ${videoSize!!.first} height: ${videoSize!!.second}")
videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt()
Logd(TAG, "setupVideoAspectRatio Width,height of video 1: $videoWidth, $videoHeight")
Logd(TAG, "setupVideoAspectRatio adjusted video width: $videoWidth height: $videoHeight")
} else {
Log.e(TAG, "setupVideoAspectRatio Could not determine video size")
videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
Logd(TAG, "setupVideoAspectRatio Width,height of video 2: $videoWidth, $videoHeight")
Logd(TAG, "setupVideoAspectRatio Could not determine video size, use: $videoWidth $videoHeight")
}
val lp = binding.videoView.layoutParams
lp.width = videoWidth
@ -924,9 +926,7 @@ class VideoplayerActivity : CastEnabledActivity() {
companion object {
private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous"
const val VIDEO_MODE = "Video_Mode"
var videoMode = VideoMode.None
var videoMode = VideoMode.NONE
private val audioTracks: List<String>
get() {

View File

@ -2,8 +2,6 @@ package ac.mdiq.podcini.ui.activity.starter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.VIDEO_MODE
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@ -14,7 +12,7 @@ import androidx.media3.common.util.UnstableApi
* Launches the video player activity of the app with specific arguments.
* Does not require a dependency on the actual implementation of the activity.
*/
@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) {
@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context) {
val intent: Intent = Intent(INTENT)
val pendingIntent: PendingIntent
get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent,
@ -23,7 +21,6 @@ import androidx.media3.common.util.UnstableApi
init {
intent.setPackage(context.packageName)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode)
}
fun start() {

View File

@ -5,17 +5,18 @@ import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.toggleFallbackSpeed
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
@ -26,16 +27,12 @@ import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.model.Chapter
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.ChapterUtils
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
@ -475,7 +472,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
R.id.show_video -> {
playPause()
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
VideoPlayerActivityStarter(requireContext()).start()
}
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
R.id.open_feed_item -> {
@ -550,8 +547,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val media = curMedia
if (media != null) {
val mediaType = media.getMediaType()
if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY
|| (media is EpisodeMedia && media.episode?.feed?.preferences?.playAudioOnly == true)) {
if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY
|| (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) {
ensureService()
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else {
@ -577,7 +574,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
if (curMedia != null) {
val media = curMedia!!
if (media.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING &&
(media is EpisodeMedia && media.episode?.feed?.preferences?.playAudioOnly != true)) {
(media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY)) {
playPause()
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
} else playPause()

View File

@ -5,14 +5,15 @@ import ac.mdiq.podcini.databinding.AutodownloadFilterDialogBinding
import ac.mdiq.podcini.databinding.FeedsettingsBinding
import ac.mdiq.podcini.databinding.PlaybackSpeedFeedSettingDialogBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.base.VideoMode.Companion.videoModeTags
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDLPolicy
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.*
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions
import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter
import ac.mdiq.podcini.ui.compose.CustomTheme
@ -64,7 +65,9 @@ class FeedSettingsFragment : Fragment() {
private var feed: Feed? = null
private var autoDeleteSummaryResId by mutableIntStateOf(R.string.global_default)
private var curPrefQueue by mutableStateOf(feed?.preferences?.queueTextExt ?: "Default")
private var autoDeletePolicy = "global"
private var autoDeletePolicy = AutoDeleteAction.GLOBAL.name
private var videoModeSummaryResId by mutableIntStateOf(R.string.global_default)
private var videoMode = VideoMode.NONE.name
private var queues: List<PlayQueue>? = null
private var notificationPermissionDenied: Boolean = false
@ -88,6 +91,7 @@ class FeedSettingsFragment : Fragment() {
val toolbar = binding.toolbar
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
getVideoModePolicy()
getAutoDeletePolicy()
binding.composeView.setContent {
@ -130,30 +134,27 @@ class FeedSettingsFragment : Fragment() {
// prefer play audio only
Column {
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24),
"",
tint = textColor)
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(
text = stringResource(R.string.pref_audio_only_title),
text = stringResource(R.string.feed_video_mode_label),
style = MaterialTheme.typography.h6,
color = textColor
)
Spacer(modifier = Modifier.weight(1f))
var checked by remember { mutableStateOf(feed?.preferences?.playAudioOnly ?: false) }
Switch(
checked = checked,
modifier = Modifier.height(24.dp),
onCheckedChange = {
checked = it
feed = upsertBlk(feed!!) { f ->
f.preferences?.playAudioOnly = checked
color = textColor,
modifier = Modifier.clickable(onClick = {
val composeView = ComposeView(requireContext()).apply {
setContent {
val showDialog = remember { mutableStateOf(true) }
CustomTheme(requireContext()) {
VideoModeDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
}
}
}
}
(view as? ViewGroup)?.addView(composeView)
})
)
}
Text(
text = stringResource(R.string.pref_audio_only_sum),
text = stringResource(videoModeSummaryResId),
style = MaterialTheme.typography.body2,
color = textColor
)
@ -270,8 +271,7 @@ class FeedSettingsFragment : Fragment() {
setContent {
val showDialog = remember { mutableStateOf(true) }
CustomTheme(requireContext()) {
AutoDeleteDialog(showDialog.value,
onDismissRequest = { showDialog.value = false })
AutoDeleteDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
}
}
}
@ -596,19 +596,94 @@ class FeedSettingsFragment : Fragment() {
super.onDestroyView()
}
private fun getVideoModePolicy() {
when (feed?.preferences!!.videoModePolicy) {
VideoMode.NONE -> {
videoModeSummaryResId = R.string.global_default
videoMode = VideoMode.NONE.tag
}
VideoMode.WINDOW_VIEW -> {
videoModeSummaryResId = R.string.feed_video_mode_window
videoMode = VideoMode.WINDOW_VIEW.tag
}
VideoMode.FULL_SCREEN_VIEW -> {
videoModeSummaryResId = R.string.feed_video_mode_fullscreen
videoMode = VideoMode.FULL_SCREEN_VIEW.tag
}
VideoMode.AUDIO_ONLY -> {
videoModeSummaryResId = R.string.feed_video_mode_audioonly
videoMode = VideoMode.AUDIO_ONLY.tag
}
}
}
@Composable
fun VideoModeDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(
modifier = Modifier
.wrapContentSize(align = Alignment.Center)
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Column {
videoModeTags.forEach { text ->
Row(Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = (text == selectedOption),
onCheckedChange = {
Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
val mode_ = when (text) {
VideoMode.NONE.tag -> VideoMode.NONE
VideoMode.WINDOW_VIEW.tag -> VideoMode.WINDOW_VIEW
VideoMode.FULL_SCREEN_VIEW.tag -> VideoMode.FULL_SCREEN_VIEW
VideoMode.AUDIO_ONLY.tag -> VideoMode.AUDIO_ONLY
else -> VideoMode.NONE
}
feed = upsertBlk(feed!!) { it.preferences?.videoModePolicy = mode_ }
getVideoModePolicy()
onDismissRequest()
}
}
)
Text(
text = text,
style = MaterialTheme.typography.body1.merge(),
// color = textColor,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
}
}
}
}
private fun getAutoDeletePolicy() {
when (feed?.preferences!!.autoDeleteAction) {
AutoDeleteAction.GLOBAL -> {
autoDeleteSummaryResId = R.string.global_default
autoDeletePolicy = "global"
autoDeletePolicy = AutoDeleteAction.GLOBAL.tag
}
AutoDeleteAction.ALWAYS -> {
autoDeleteSummaryResId = R.string.feed_auto_download_always
autoDeletePolicy = "always"
autoDeletePolicy = AutoDeleteAction.ALWAYS.tag
}
AutoDeleteAction.NEVER -> {
autoDeleteSummaryResId = R.string.feed_auto_download_never
autoDeletePolicy = "never"
autoDeletePolicy = AutoDeleteAction.NEVER.tag
}
}
}
@ -640,14 +715,12 @@ class FeedSettingsFragment : Fragment() {
if (text != selectedOption) {
onOptionSelected(text)
val action_ = when (text) {
"global" -> AutoDeleteAction.GLOBAL
"always" -> AutoDeleteAction.ALWAYS
"never" -> AutoDeleteAction.NEVER
AutoDeleteAction.GLOBAL.tag -> AutoDeleteAction.GLOBAL
AutoDeleteAction.ALWAYS.tag -> AutoDeleteAction.ALWAYS
AutoDeleteAction.NEVER.tag -> AutoDeleteAction.NEVER
else -> AutoDeleteAction.GLOBAL
}
feed = upsertBlk(feed!!) {
it.preferences?.autoDeleteAction = action_
}
feed = upsertBlk(feed!!) { it.preferences?.autoDeleteAction = action_ }
getAutoDeletePolicy()
onDismissRequest()
}
@ -720,7 +793,7 @@ class FeedSettingsFragment : Fragment() {
@Composable
fun AutoDownloadPolicyDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDLPolicy.ONLY_NEW) }
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(
modifier = Modifier
@ -733,7 +806,7 @@ class FeedSettingsFragment : Fragment() {
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Column {
AutoDLPolicy.entries.forEach { item ->
AutoDownloadPolicy.entries.forEach { item ->
Row(Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),

View File

@ -67,7 +67,6 @@ class ShownotesWebView : WebView, View.OnLongClickListener {
else IntentUtils.openInBrowser(context, url)
return true
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
Logd(TAG, "Page finished")
@ -88,9 +87,8 @@ class ShownotesWebView : WebView, View.OnLongClickListener {
HitTestResult.EMAIL_TYPE -> {
Logd(TAG, "E-Mail of webview was long-pressed. Extra: " + r.extra)
ContextCompat.getSystemService(context, ClipboardManager::class.java)?.setPrimaryClip(ClipData.newPlainText("Podcini", r.extra))
if (Build.VERSION.SDK_INT <= 32 && this.context is MainActivity) {
if (Build.VERSION.SDK_INT <= 32 && this.context is MainActivity)
(this.context as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
}
return true
}
else -> {

View File

@ -142,6 +142,11 @@
<string name="feed_auto_download_always">Always</string>
<string name="feed_auto_download_never">Never</string>
<string name="feed_video_mode_label">Video mode</string>
<string name="feed_video_mode_window">Window mode</string>
<string name="feed_video_mode_fullscreen">Full screen</string>
<string name="feed_video_mode_audioonly">Audio only</string>
<string name="feed_auto_download_policy">Auto download policy</string>
<string name="feed_auto_download_new">Only new items</string>
<string name="feed_auto_download_newer">Newest unplayed</string>

View File

@ -1,3 +1,9 @@
# 6.5.6
* in feed preferences, the setting "play audio only" for video feed is replaced with the setting of a video mode. If you set the previous setting, you need to redo with the new setting.
* added some extra permission requests when exporting/importing various files, maybe needed in some system
* re-enabed use of http traffic to work with relevant podcasts
# 6.5.5
* corrected issue of Youtube channel being set for auto-download when subscribing

View File

@ -0,0 +1,5 @@
Version 6.5.6 brings several changes:
* in feed preferences, the setting "play audio only" for video feed is replaced with the setting of a video mode. If you set the previous setting, you need to redo with the new setting.
* added some extra permission requests when exporting/importing various files, maybe needed in some system
* re-enabed use of http traffic to work with relevant podcasts