6.13.0 commit
This commit is contained in:
parent
38a27080a4
commit
69de20fd35
|
@ -28,11 +28,11 @@ android {
|
|||
vectorDrawables.useSupportLibrary false
|
||||
vectorDrawables.generatedDensities = []
|
||||
|
||||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
// testApplicationId "ac.mdiq.podcini.tests"
|
||||
// testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020286
|
||||
versionName "6.12.8"
|
||||
versionCode 3020287
|
||||
versionName "6.13.0"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -29,14 +29,14 @@ object InTheatre {
|
|||
value != null -> {
|
||||
field = unmanaged(value)
|
||||
if (field?.media != null && curMedia?.getIdentifier() != field?.media?.getIdentifier()) curMedia = unmanaged(field!!.media!!)
|
||||
// field = value
|
||||
// if (field?.media != null && curMedia?.getIdentifier() != field?.media?.getIdentifier()) curMedia = field!!.media!!
|
||||
}
|
||||
else -> {
|
||||
field = null
|
||||
if (curMedia != null) curMedia = null
|
||||
}
|
||||
}
|
||||
// field = if (value != null) unmanaged(value) else null
|
||||
// if (field?.media != null && curMedia?.getIdentifier() != field?.media?.getIdentifier()) curMedia = unmanaged(field!!.media!!)
|
||||
}
|
||||
|
||||
var curMedia: Playable? = null // unmanged if EpisodeMedia
|
||||
|
@ -45,6 +45,8 @@ object InTheatre {
|
|||
value is EpisodeMedia -> {
|
||||
field = unmanaged(value)
|
||||
if (value.episode != null && curEpisode?.id != value.episode?.id) curEpisode = unmanaged(value.episode!!)
|
||||
// field = value
|
||||
// if (value.episode != null && curEpisode?.id != value.episode?.id) curEpisode = value.episode!!
|
||||
}
|
||||
value == null -> {
|
||||
field = null
|
||||
|
@ -52,12 +54,6 @@ object InTheatre {
|
|||
}
|
||||
else -> field = value
|
||||
}
|
||||
// if (value is EpisodeMedia) {
|
||||
// field = unmanaged(value)
|
||||
// if (value.episode != null && curEpisode?.id != value.episode?.id) curEpisode = unmanaged(value.episode!!)
|
||||
// } else {
|
||||
// field = value
|
||||
// }
|
||||
}
|
||||
|
||||
var curState: CurrentState // managed
|
||||
|
|
|
@ -13,10 +13,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
|||
import ac.mdiq.podcini.preferences.UserPreferences.prefLowQualityMedia
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
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
|
||||
|
@ -518,10 +515,16 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
Logd(TAG, "setVolume: $volumeLeft $volumeRight")
|
||||
val playable = curMedia
|
||||
if (playable is EpisodeMedia) {
|
||||
var adaptionFactor = 1f
|
||||
if (playable.volumeAdaptionSetting != VolumeAdaptionSetting.OFF) adaptionFactor = playable.volumeAdaptionSetting.adaptionFactor
|
||||
else {
|
||||
val preferences = playable.episodeOrFetch()?.feed?.preferences
|
||||
if (preferences != null) {
|
||||
val volumeAdaptionSetting = preferences.volumeAdaptionSetting
|
||||
val adaptionFactor = volumeAdaptionSetting.adaptionFactor
|
||||
adaptionFactor = volumeAdaptionSetting.adaptionFactor
|
||||
}
|
||||
}
|
||||
if (adaptionFactor != 1f) {
|
||||
volumeLeft *= adaptionFactor
|
||||
volumeRight *= adaptionFactor
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybac
|
|||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
||||
import ac.mdiq.podcini.playback.cast.CastMediaPlayer
|
||||
import ac.mdiq.podcini.playback.cast.CastStateListener
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.TaskManager.Companion.positionUpdateInterval
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
|
||||
|
@ -28,6 +29,7 @@ import ac.mdiq.podcini.preferences.UserPreferences
|
|||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.prefAdaptiveProgressUpdate
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
|
||||
|
@ -369,7 +371,7 @@ class PlaybackService : MediaLibraryService() {
|
|||
val shouldAutoDelete = (action == AutoDeleteAction.ALWAYS ||
|
||||
(action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!)))
|
||||
if (playable is EpisodeMedia && shouldAutoDelete && (item?.isSUPER != true || !shouldFavoriteKeepEpisode)) {
|
||||
item = deleteMediaSync(this@PlaybackService, item!!)
|
||||
if (playable.localFileAvailable()) item = deleteMediaSync(this@PlaybackService, item!!)
|
||||
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item!!)
|
||||
}
|
||||
}
|
||||
|
@ -380,11 +382,12 @@ class PlaybackService : MediaLibraryService() {
|
|||
|
||||
override fun onPlaybackStart(playable: Playable, position: Int) {
|
||||
Logd(TAG, "onPlaybackStart position: $position")
|
||||
taskManager.startWidgetUpdater()
|
||||
if (position != Playable.INVALID_TIME) playable.setPosition(position)
|
||||
val delayInterval = positionUpdateInterval(playable.getDuration())
|
||||
taskManager.startWidgetUpdater(delayInterval)
|
||||
if (position != Playable.INVALID_TIME) playable.setPosition(position + (delayInterval/2).toInt())
|
||||
else skipIntro(playable)
|
||||
playable.onPlaybackStart()
|
||||
taskManager.startPositionSaver()
|
||||
taskManager.startPositionSaver(delayInterval)
|
||||
}
|
||||
|
||||
override fun onPlaybackPause(playable: Playable?, position: Int) {
|
||||
|
@ -1310,12 +1313,14 @@ class PlaybackService : MediaLibraryService() {
|
|||
get() = positionSaverFuture != null && !positionSaverFuture!!.isCancelled && !positionSaverFuture!!.isDone
|
||||
|
||||
@Synchronized
|
||||
fun startPositionSaver() {
|
||||
fun startPositionSaver(delayInterval: Long) {
|
||||
if (!isPositionSaverActive) {
|
||||
var positionSaver = Runnable { callback.positionSaverTick() }
|
||||
positionSaver = useMainThreadIfNecessary(positionSaver)
|
||||
positionSaverFuture = schedExecutor.scheduleWithFixedDelay(
|
||||
positionSaver, POSITION_SAVER_WAITING_INTERVAL.toLong(), POSITION_SAVER_WAITING_INTERVAL.toLong(), TimeUnit.MILLISECONDS)
|
||||
// val delayInterval = positionUpdateInterval(duration)
|
||||
// positionSaverFuture = schedExecutor.scheduleWithFixedDelay(
|
||||
// positionSaver, POSITION_SAVER_WAITING_INTERVAL.toLong(), POSITION_SAVER_WAITING_INTERVAL.toLong(), TimeUnit.MILLISECONDS)
|
||||
positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, delayInterval, delayInterval, TimeUnit.MILLISECONDS)
|
||||
Logd(TAG, "Started PositionSaver")
|
||||
} else Logd(TAG, "Call to startPositionSaver was ignored.")
|
||||
}
|
||||
|
@ -1329,12 +1334,14 @@ class PlaybackService : MediaLibraryService() {
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
fun startWidgetUpdater() {
|
||||
fun startWidgetUpdater(delayInterval: Long) {
|
||||
if (!isWidgetUpdaterActive && !schedExecutor.isShutdown) {
|
||||
var widgetUpdater = Runnable { this.requestWidgetUpdate() }
|
||||
widgetUpdater = useMainThreadIfNecessary(widgetUpdater)
|
||||
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(
|
||||
widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), TimeUnit.MILLISECONDS)
|
||||
// val delayInterval = positionUpdateInterval(duration)
|
||||
// widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(
|
||||
// widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), TimeUnit.MILLISECONDS)
|
||||
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, delayInterval, delayInterval, TimeUnit.MILLISECONDS)
|
||||
Logd(TAG, "Started WidgetUpdater")
|
||||
}
|
||||
}
|
||||
|
@ -1401,7 +1408,6 @@ class PlaybackService : MediaLibraryService() {
|
|||
fun startChapterLoader(media: Playable) {
|
||||
// chapterLoaderFuture?.dispose()
|
||||
// chapterLoaderFuture = null
|
||||
|
||||
if (!media.chaptersLoaded()) {
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
@ -1423,7 +1429,6 @@ class PlaybackService : MediaLibraryService() {
|
|||
cancelPositionSaver()
|
||||
cancelWidgetUpdater()
|
||||
disableSleepTimer()
|
||||
|
||||
// chapterLoaderFuture?.dispose()
|
||||
// chapterLoaderFuture = null
|
||||
}
|
||||
|
@ -1557,8 +1562,13 @@ class PlaybackService : MediaLibraryService() {
|
|||
|
||||
private const val SLEEP_TIMER_UPDATE_INTERVAL = 10000L // in millisoconds
|
||||
const val POSITION_SAVER_WAITING_INTERVAL: Int = 5000 // in millisoconds
|
||||
const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 5000 // in millisoconds
|
||||
// const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 5000 // in millisoconds
|
||||
const val NOTIFICATION_THRESHOLD: Long = 10000 // in millisoconds
|
||||
|
||||
fun positionUpdateInterval(duration: Int): Long {
|
||||
return if (prefAdaptiveProgressUpdate) max(POSITION_SAVER_WAITING_INTERVAL, duration/50).toLong()
|
||||
else POSITION_SAVER_WAITING_INTERVAL.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -203,6 +203,12 @@ object UserPreferences {
|
|||
appPrefs.edit().putBoolean(Prefs.prefLowQualityOnMobile.name, stream).apply()
|
||||
}
|
||||
|
||||
var prefAdaptiveProgressUpdate: Boolean
|
||||
get() = appPrefs.getBoolean(Prefs.prefUseAdaptiveProgressUpdate.name, false)
|
||||
set(value) {
|
||||
appPrefs.edit().putBoolean(Prefs.prefUseAdaptiveProgressUpdate.name, value).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the UserPreferences class.
|
||||
* @throws IllegalArgumentException if context is null
|
||||
|
@ -313,6 +319,7 @@ object UserPreferences {
|
|||
prefStreamOverDownload,
|
||||
prefLowQualityOnMobile,
|
||||
prefSpeedforwardSpeed,
|
||||
prefUseAdaptiveProgressUpdate,
|
||||
|
||||
// Network
|
||||
prefEnqueueDownloaded,
|
||||
|
|
|
@ -126,6 +126,10 @@ object AutoDownloads {
|
|||
queryString += " AND playState <= ${PlayState.SOON.code} SORT(pubDate DESC) LIMIT(${3*allowedDLCount})"
|
||||
episodes = realm.query(Episode::class).query(queryString).find().toMutableList()
|
||||
}
|
||||
FeedPreferences.AutoDownloadPolicy.SOON -> {
|
||||
queryString += " AND playState == ${PlayState.SOON.code} SORT(pubDate DESC) LIMIT(${3*allowedDLCount})"
|
||||
episodes = realm.query(Episode::class).query(queryString).find().toMutableList()
|
||||
}
|
||||
FeedPreferences.AutoDownloadPolicy.OLDER -> {
|
||||
queryString += " AND playState <= ${PlayState.SOON.code} SORT(pubDate ASC) LIMIT(${3*allowedDLCount})"
|
||||
episodes = realm.query(Episode::class).query(queryString).find().toMutableList()
|
||||
|
@ -136,7 +140,7 @@ object AutoDownloads {
|
|||
var count = 0
|
||||
for (e in episodes) {
|
||||
if (isCurMedia(e.media)) continue
|
||||
if (f.preferences?.autoDownloadFilter?.shouldAutoDownload(e) == true) {
|
||||
if (f.preferences?.autoDownloadFilter?.meetsAutoDLCriteria(e) == true) {
|
||||
Logd(TAG, "autoDownloadEpisodeMedia add to cadidates: ${e.title} ${e.isDownloaded}")
|
||||
candidates.add(e)
|
||||
if (++count >= allowedDLCount) break
|
||||
|
|
|
@ -129,7 +129,7 @@ object Episodes {
|
|||
episode = upsertBlk(episode) {
|
||||
it.media?.setfileUrlOrNull(null)
|
||||
if (media.downloadUrl.isNullOrEmpty()) it.media = null
|
||||
it.playState = PlayState.SKIPPED.code
|
||||
if (it.playState < PlayState.SKIPPED.code) it.playState = PlayState.SKIPPED.code
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(episode))
|
||||
localDelete = true
|
||||
|
@ -139,7 +139,7 @@ object Episodes {
|
|||
val mediaFile = File(url)
|
||||
if (!mediaFile.delete()) {
|
||||
Log.e(TAG, "delete media file failed: $url")
|
||||
val evt = FlowEvent.MessageEvent(context.getString(R.string.delete_failed))
|
||||
val evt = FlowEvent.MessageEvent(context.getString(R.string.delete_failed_simple) + ": $url")
|
||||
EventFlow.postEvent(evt)
|
||||
return episode
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ object Episodes {
|
|||
it.media?.downloaded = false
|
||||
it.media?.setfileUrlOrNull(null)
|
||||
it.media?.hasEmbeddedPicture = false
|
||||
it.playState = PlayState.SKIPPED.code
|
||||
if (it.playState < PlayState.SKIPPED.code) it.playState = PlayState.SKIPPED.code
|
||||
if (media.downloadUrl.isNullOrEmpty()) it.media = null
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(episode))
|
||||
|
|
|
@ -3,6 +3,7 @@ package ac.mdiq.podcini.storage.model
|
|||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger
|
||||
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.showStackTrace
|
||||
|
@ -68,6 +69,16 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
var playedDurationWhenStarted: Int = 0
|
||||
private set
|
||||
|
||||
@Ignore
|
||||
var volumeAdaptionSetting: VolumeAdaptionSetting = VolumeAdaptionSetting.OFF
|
||||
get() = fromInteger(volumeAdaption)
|
||||
set(value) {
|
||||
field = value
|
||||
volumeAdaption = field.toInteger()
|
||||
}
|
||||
@Ignore
|
||||
var volumeAdaption: Int = 0
|
||||
|
||||
// if null: unknown, will be checked
|
||||
// TODO: what to do with this? can be expensive
|
||||
@Ignore
|
||||
|
|
|
@ -35,7 +35,7 @@ class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilt
|
|||
* @param item
|
||||
* @return true if the item should be downloaded
|
||||
*/
|
||||
fun shouldAutoDownload(item: Episode): Boolean {
|
||||
fun meetsAutoDLCriteria(item: Episode): Boolean {
|
||||
// if (includeTerms == null) includeTerms = parseTerms(includeFilterRaw)
|
||||
// if (excludeTerms == null) excludeTerms = parseTerms(excludeFilterRaw)
|
||||
|
||||
|
|
|
@ -172,7 +172,8 @@ class FeedPreferences : EmbeddedRealmObject {
|
|||
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);
|
||||
OLDER(2, R.string.feed_auto_download_older),
|
||||
SOON(3, R.string.feed_auto_download_soon);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int): AutoDownloadPolicy {
|
||||
|
|
|
@ -7,9 +7,6 @@ enum class VolumeAdaptionSetting(private val value: Int, @JvmField val adaptionF
|
|||
LIGHT_REDUCTION(1, 0.5f, R.string.feed_volume_reduction_light),
|
||||
HEAVY_REDUCTION(2, 0.2f, R.string.feed_volume_reduction_heavy),
|
||||
LIGHT_BOOST(3, 1.6f, R.string.feed_volume_boost_light),
|
||||
// MEDIUM_BOOST(4, 2f, R.string.feed_volume_boost_medium),
|
||||
// HEAVY_BOOST(5, 2.5f, R.string.feed_volume_boost_heavy);
|
||||
// LIGHT_BOOST(3, 2f, R.string.feed_volume_boost_light),
|
||||
MEDIUM_BOOST(4, 2.4f, R.string.feed_volume_boost_medium),
|
||||
HEAVY_BOOST(5, 3.6f, R.string.feed_volume_boost_heavy);
|
||||
|
||||
|
|
|
@ -81,7 +81,8 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
|
|||
|
||||
abstract fun onClick(context: Context)
|
||||
|
||||
fun forItem(): EpisodeActionButton {
|
||||
open fun forItem(item_: Episode): EpisodeActionButton {
|
||||
item = item_
|
||||
val media = item.media ?: return TTSActionButton(item)
|
||||
val isDownloadingMedia = when (media.downloadUrl) {
|
||||
null -> false
|
||||
|
@ -189,6 +190,11 @@ class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
if (!item.link.isNullOrEmpty()) IntentUtils.openInBrowser(context, item.link!!)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
override fun forItem(item_: Episode): EpisodeActionButton {
|
||||
item = item_
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
class CancelDownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
@ -358,6 +364,21 @@ class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
val isDownloading = DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false
|
||||
return isDownloading || media.downloaded
|
||||
}
|
||||
|
||||
// override fun forItem(item_: Episode): EpisodeActionButton {
|
||||
// item = item_
|
||||
// val media = item.media ?: return TTSActionButton(item)
|
||||
// val isDownloadingMedia = when (media.downloadUrl) {
|
||||
// null -> false
|
||||
// else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false
|
||||
// }
|
||||
// Logd("DownloadActionButton", "forItem: local feed: ${item.feed?.isLocalFeed} downloaded: ${media.downloaded} playing: ${isCurrentlyPlaying(media)} ${item.title} ")
|
||||
// return when {
|
||||
// media.downloaded -> PlayActionButton(item)
|
||||
// isDownloadingMedia -> CancelDownloadActionButton(item)
|
||||
// else -> DownloadActionButton(item)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
|
|
@ -20,16 +20,20 @@ interface SwipeAction {
|
|||
|
||||
fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter)
|
||||
|
||||
fun willRemove(filter: EpisodeFilter, item: Episode): Boolean
|
||||
fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
enum class ActionTypes {
|
||||
NO_ACTION,
|
||||
COMBO,
|
||||
ADD_TO_QUEUE,
|
||||
PUT_TO_QUEUE,
|
||||
START_DOWNLOAD,
|
||||
MARK_FAV,
|
||||
TOGGLE_PLAYED,
|
||||
SET_PLAY_STATE,
|
||||
SHELVE,
|
||||
ERASE,
|
||||
REMOVE_FROM_QUEUE,
|
||||
DELETE,
|
||||
REMOVE_FROM_HISTORY
|
||||
|
|
|
@ -21,9 +21,7 @@ import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
|||
import ac.mdiq.podcini.ui.actions.SwipeAction.ActionTypes
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction.ActionTypes.NO_ACTION
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.PlayStateDialog
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
|
@ -77,22 +75,12 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
actions = getPrefs(tag)
|
||||
}
|
||||
|
||||
// override fun onStop(owner: LifecycleOwner) {
|
||||
//// actions = null
|
||||
// }
|
||||
|
||||
@JvmName("setFilterFunction")
|
||||
fun setFilter(filter: EpisodeFilter?) {
|
||||
this.filter = filter
|
||||
}
|
||||
|
||||
fun showDialog() {
|
||||
// SwipeActionsDialog(fragment.requireContext(), tag).show(object : SwipeActionsDialog.Callback {
|
||||
// override fun onCall() {
|
||||
// actions = getPrefs(tag)
|
||||
// EventFlow.postEvent(FlowEvent.SwipeActionsChangedEvent())
|
||||
// }
|
||||
// })
|
||||
Logd("SwipeActions", "showDialog()")
|
||||
val composeView = ComposeView(fragment.requireContext()).apply {
|
||||
setContent {
|
||||
|
@ -133,47 +121,34 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
override fun getId(): String {
|
||||
return ActionTypes.ADD_TO_QUEUE.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_playlist_play
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return androidx.appcompat.R.attr.colorAccent
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.add_to_queue_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
addToQueue(item)
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
// return filter.showQueued || filter.showNew
|
||||
}
|
||||
}
|
||||
|
||||
class ComboSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return ActionTypes.COMBO.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.baseline_category_24
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return androidx.appcompat.R.attr.colorAccent
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.add_to_queue_label)
|
||||
return context.getString(R.string.combo_action)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val composeView = ComposeView(fragment.requireContext()).apply {
|
||||
|
@ -210,36 +185,26 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
}
|
||||
(fragment.view as? ViewGroup)?.addView(composeView)
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
// return filter.showQueued || filter.showNew
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return ActionTypes.DELETE.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_delete
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_red
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.delete_episode_label)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
if (!item.isDownloaded && item.feed?.isLocalFeed != true) return
|
||||
deleteEpisodesWarnLocal(fragment.requireContext(), listOf(item))
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showDownloaded && (item.isDownloaded || item.feed?.isLocalFeed == true)
|
||||
}
|
||||
|
@ -249,19 +214,15 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
override fun getId(): String {
|
||||
return ActionTypes.MARK_FAV.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_star
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_yellow
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.switch_rating_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
var showChooseRatingDialog by mutableStateOf(true)
|
||||
|
@ -277,36 +238,22 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
}
|
||||
(fragment.view as? ViewGroup)?.addView(composeView)
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
// return filter.showIsFavorite || filter.showNotFavorite
|
||||
}
|
||||
}
|
||||
|
||||
class NoActionSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return NO_ACTION.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_questionmark
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_red
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.no_action_label)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveFromHistorySwipeAction : SwipeAction {
|
||||
|
@ -315,19 +262,15 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
override fun getId(): String {
|
||||
return ActionTypes.REMOVE_FROM_HISTORY.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_history_remove
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_purple
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.remove_history_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val playbackCompletionDate: Date? = item.media?.playbackCompletionDate
|
||||
|
@ -339,11 +282,9 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
.setAction(fragment.getString(R.string.undo)) {
|
||||
if (playbackCompletionDate != null) setHistoryDates(item, lastPlayedDate?:0, playbackCompletionDate) }
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setHistoryDates(episode: Episode, lastPlayed: Long = 0, completed: Date = Date(0)) {
|
||||
runOnIOScope {
|
||||
val episode_ = realm.query(Episode::class).query("id == $0", episode.id).first().find()
|
||||
|
@ -362,19 +303,15 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
override fun getId(): String {
|
||||
return ActionTypes.REMOVE_FROM_QUEUE.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_playlist_remove
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return androidx.appcompat.R.attr.colorAccent
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.remove_from_queue_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val position: Int = curQueue.episodes.indexOf(item)
|
||||
|
@ -386,11 +323,9 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showQueued || filter.showNotQueued
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a Episode in the queue at the specified index. The 'read'-attribute of the Episode will be set to
|
||||
* true. If the Episode is already in the queue, the queue will not be modified.
|
||||
|
@ -414,51 +349,68 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
}
|
||||
}
|
||||
|
||||
class PutToQueueSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return ActionTypes.PUT_TO_QUEUE.name
|
||||
}
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_playlist_play
|
||||
}
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.put_in_queue_label)
|
||||
}
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
var showPutToQueueDialog by mutableStateOf(true)
|
||||
val composeView = ComposeView(fragment.requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(fragment.requireContext()) {
|
||||
if (showPutToQueueDialog ) PutToQueueDialog(listOf(item)) {
|
||||
showPutToQueueDialog = false
|
||||
(fragment.view as? ViewGroup)?.removeView(this@apply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(fragment.view as? ViewGroup)?.addView(composeView)
|
||||
}
|
||||
}
|
||||
|
||||
class StartDownloadSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return ActionTypes.START_DOWNLOAD.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_download
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_green
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.download_label)
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
if (!item.isDownloaded && item.feed != null && !item.feed!!.isLocalFeed) {
|
||||
DownloadActionButton(item).onClick(fragment.requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class SetPlaybackStateSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return ActionTypes.SET_PLAY_STATE.name
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_mark_played
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.set_play_state_label)
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
var showPlayStateDialog by mutableStateOf(true)
|
||||
val composeView = ComposeView(fragment.requireContext()).apply {
|
||||
|
@ -473,21 +425,72 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
}
|
||||
(fragment.view as? ViewGroup)?.addView(composeView)
|
||||
}
|
||||
|
||||
private fun delayedExecution(item: Episode, fragment: Fragment, duration: Float) = runBlocking {
|
||||
delay(ceil((duration * 1.05f).toDouble()).toLong())
|
||||
val media: EpisodeMedia? = item.media
|
||||
val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
|
||||
if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
|
||||
// deleteMediaOfEpisode(fragment.requireContext(), item)
|
||||
var item = deleteMediaSync(fragment.requireContext(), item)
|
||||
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) }
|
||||
val item_ = deleteMediaSync(fragment.requireContext(), item)
|
||||
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item_) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
// return if (item.playState == PlayState.NEW.code) filter.showPlayed || filter.showNew
|
||||
// else filter.showUnplayed || filter.showPlayed || filter.showNew
|
||||
class ShelveSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return ActionTypes.SHELVE.name
|
||||
}
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.baseline_shelves_24
|
||||
}
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.shelve_label)
|
||||
}
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
var showShelveDialog by mutableStateOf(true)
|
||||
val composeView = ComposeView(fragment.requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(fragment.requireContext()) {
|
||||
if (showShelveDialog) ShelveDialog(listOf(item)) {
|
||||
showShelveDialog = false
|
||||
(fragment.view as? ViewGroup)?.removeView(this@apply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(fragment.view as? ViewGroup)?.addView(composeView)
|
||||
}
|
||||
}
|
||||
|
||||
class EraseSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return ActionTypes.ERASE.name
|
||||
}
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.baseline_delete_forever_24
|
||||
}
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.erase_episodes_label)
|
||||
}
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
var showEraseDialog by mutableStateOf(true)
|
||||
val composeView = ComposeView(fragment.requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(fragment.requireContext()) {
|
||||
if (showEraseDialog) EraseEpisodesDialog(listOf(item), item.feed) {
|
||||
showEraseDialog = false
|
||||
(fragment.view as? ViewGroup)?.removeView(this@apply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(fragment.view as? ViewGroup)?.addView(composeView)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,10 +506,12 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
|
||||
@JvmField
|
||||
val swipeActions: List<SwipeAction> = listOf(
|
||||
NoActionSwipeAction(), ComboSwipeAction(), AddToQueueSwipeAction(),
|
||||
NoActionSwipeAction(), ComboSwipeAction(),
|
||||
AddToQueueSwipeAction(), PutToQueueSwipeAction(),
|
||||
StartDownloadSwipeAction(), SetRatingSwipeAction(),
|
||||
SetPlaybackStateSwipeAction(), RemoveFromQueueSwipeAction(),
|
||||
DeleteSwipeAction(), RemoveFromHistorySwipeAction())
|
||||
DeleteSwipeAction(), RemoveFromHistorySwipeAction(),
|
||||
ShelveSwipeAction(), EraseSwipeAction())
|
||||
|
||||
private fun getPrefs(tag: String, defaultActions: String): Actions {
|
||||
val prefsString = prefs!!.getString(KEY_PREFIX_SWIPEACTIONS + tag, defaultActions)
|
||||
|
@ -609,9 +614,9 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
}
|
||||
QueuesFragment.TAG -> {
|
||||
forFragment = stringResource(R.string.queue_label)
|
||||
// keys = Stream.of(keys).filter { a: SwipeAction ->
|
||||
// (!a.getId().equals(SwipeAction.ADD_TO_QUEUE) && !a.getId().equals(SwipeAction.REMOVE_FROM_HISTORY)) }.toList()
|
||||
keys = keys.filter { a: SwipeAction -> (!a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name)) }
|
||||
keys = keys.filter { a: SwipeAction ->
|
||||
(!a.getId().equals(ActionTypes.ADD_TO_QUEUE.name) && !a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name)) }.toList()
|
||||
// keys = keys.filter { a: SwipeAction -> (!a.getId().equals(ActionTypes.REMOVE_FROM_HISTORY.name)) }
|
||||
}
|
||||
HistoryFragment.TAG -> {
|
||||
forFragment = stringResource(R.string.playback_history_label)
|
||||
|
|
|
@ -65,6 +65,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
|
@ -366,6 +367,7 @@ fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
.padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
var removeChecked by remember { mutableStateOf(false) }
|
||||
var toFeed by remember { mutableStateOf<Feed?>(null) }
|
||||
if (synthetics.isNotEmpty()) {
|
||||
for (f in synthetics) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = toFeed == f, onClick = { toFeed = f })
|
||||
|
@ -376,6 +378,7 @@ fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
Checkbox(checked = removeChecked, onCheckedChange = { removeChecked = it })
|
||||
Text(text = stringResource(R.string.remove_from_current_feed), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp))
|
||||
}
|
||||
} else Text(text = stringResource(R.string.create_synthetic_first_note))
|
||||
if (toFeed != null) Row {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = {
|
||||
|
@ -413,6 +416,58 @@ fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest: () -> Unit) {
|
||||
val message = stringResource(R.string.erase_episodes_confirmation_msg)
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
var textState by remember { mutableStateOf(TextFieldValue("")) }
|
||||
val context = LocalContext.current
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
if (feed == null || feed.id > MAX_SYNTHETIC_ID) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
|
||||
else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(message + ": ${selected.size}")
|
||||
Text(stringResource(R.string.feed_delete_reason_msg))
|
||||
BasicTextField(value = textState, onValueChange = { textState = it }, textStyle = TextStyle(fontSize = 16.sp, color = textColor),
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
|
||||
.border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
||||
)
|
||||
Button(onClick = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
for (e in selected) {
|
||||
val sLog = SubscriptionLog(e.id, e.title?:"", e.media?.downloadUrl?:"", e.link?:"", SubscriptionLog.Type.Media.name)
|
||||
upsert(sLog) {
|
||||
it.rating = e.rating
|
||||
it.comment = e.comment
|
||||
it.comment += "\nReason to remove:\n" + textState.text
|
||||
it.cancelDate = Date().time
|
||||
}
|
||||
}
|
||||
realm.write {
|
||||
for (e in selected) {
|
||||
val url = e.media?.fileUrl
|
||||
when {
|
||||
url != null && url.startsWith("content://") -> DocumentFile.fromSingleUri(context, Uri.parse(url))?.delete()
|
||||
url != null -> File(url).delete()
|
||||
}
|
||||
findLatest(feed)?.episodes?.remove(e)
|
||||
findLatest(e)?.let { delete(it) }
|
||||
}
|
||||
}
|
||||
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
|
||||
} catch (e: Throwable) { Log.e("EraseEpisodesDialog", Log.getStackTraceString(e)) }
|
||||
}
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed: Feed? = null,
|
||||
|
@ -447,59 +502,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
var showShelveDialog by remember { mutableStateOf(false) }
|
||||
if (showShelveDialog) ShelveDialog(selected) { showShelveDialog = false }
|
||||
|
||||
@Composable
|
||||
fun EraseEpisodesDialog(onDismissRequest: () -> Unit) {
|
||||
val message = stringResource(R.string.erase_episodes_confirmation_msg)
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
var textState by remember { mutableStateOf(TextFieldValue("")) }
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(message + ": ${selected.size}")
|
||||
Text(stringResource(R.string.feed_delete_reason_msg))
|
||||
BasicTextField(value = textState, onValueChange = { textState = it },
|
||||
textStyle = TextStyle(fontSize = 16.sp, color = textColor),
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
|
||||
.border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
||||
)
|
||||
Button(onClick = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
for (e in selected) {
|
||||
val sLog = SubscriptionLog(e.id, e.title?:"", e.media?.downloadUrl?:"", e.link?:"", SubscriptionLog.Type.Media.name)
|
||||
upsert(sLog) {
|
||||
it.rating = e.rating
|
||||
it.comment = e.comment
|
||||
it.comment += "\nReason to remove:\n" + textState.text
|
||||
it.cancelDate = Date().time
|
||||
}
|
||||
}
|
||||
realm.write {
|
||||
for (e in selected) {
|
||||
val url = e.media?.fileUrl
|
||||
when {
|
||||
url != null && url.startsWith("content://") -> DocumentFile.fromSingleUri(context, Uri.parse(url))?.delete()
|
||||
url != null -> File(url).delete()
|
||||
}
|
||||
findLatest(feed!!)?.episodes?.remove(e)
|
||||
findLatest(e)?.let { delete(it) }
|
||||
}
|
||||
}
|
||||
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
|
||||
} catch (e: Throwable) { Log.e("EraseEpisodesDialog", Log.getStackTraceString(e)) }
|
||||
}
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var showEraseDialog by remember { mutableStateOf(false) }
|
||||
if (showEraseDialog) EraseEpisodesDialog(onDismissRequest = { showEraseDialog = false })
|
||||
if (showEraseDialog && feed != null) EraseEpisodesDialog(selected, feed, onDismissRequest = { showEraseDialog = false })
|
||||
|
||||
@Composable
|
||||
fun EpisodeSpeedDial(modifier: Modifier = Modifier) {
|
||||
|
@ -627,7 +631,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
showEraseDialog = true
|
||||
Logd(TAG, "reserve: ${selected.size}")
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Filled.AddCircle, "Erase episodes")
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.baseline_delete_forever_24), "Erase episodes")
|
||||
Text(stringResource(id = R.string.erase_episodes_label))
|
||||
}
|
||||
}
|
||||
|
@ -722,13 +726,14 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
// Logd(TAG, "info row")
|
||||
val ratingIconRes = Rating.fromCode(vm.ratingState).res
|
||||
if (vm.ratingState != Rating.UNRATED.code)
|
||||
Icon(imageVector = ImageVector.vectorResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(18.dp).height(18.dp))
|
||||
Icon(imageVector = ImageVector.vectorResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating",
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(16.dp).height(16.dp))
|
||||
val playStateRes = PlayState.fromCode(vm.playedState).res
|
||||
Icon(imageVector = ImageVector.vectorResource(playStateRes), tint = textColor, contentDescription = "playState", modifier = Modifier.width(18.dp).height(18.dp))
|
||||
Icon(imageVector = ImageVector.vectorResource(playStateRes), tint = textColor, contentDescription = "playState", modifier = Modifier.width(16.dp).height(16.dp))
|
||||
// if (vm.inQueueState)
|
||||
// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(18.dp).height(18.dp))
|
||||
// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(16.dp).height(16.dp))
|
||||
if (vm.episode.media?.getMediaType() == MediaType.VIDEO)
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(18.dp).height(18.dp))
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(16.dp).height(16.dp))
|
||||
val curContext = LocalContext.current
|
||||
val dur = remember { vm.episode.media?.getDuration() ?: 0 }
|
||||
val durText = remember { DurationConverter.getDurationStringLong(dur) }
|
||||
|
@ -738,7 +743,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
}
|
||||
Text(vm.episode.title ?: "", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
var actionButton by remember(vm.episode.id) { mutableStateOf(vm.actionButton.forItem()) }
|
||||
var actionButton by remember(vm.episode.id) { mutableStateOf(vm.actionButton.forItem(vm.episode)) }
|
||||
fun isDownloading(): Boolean {
|
||||
return vms[index].downloadState > DownloadStatus.State.UNKNOWN.ordinal && vms[index].downloadState < DownloadStatus.State.COMPLETED.ordinal
|
||||
}
|
||||
|
@ -748,7 +753,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media?.downloadUrl ?: "") ?: 0
|
||||
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vms[index].episode.title}")
|
||||
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
|
||||
vm.actionButton = vm.actionButton.forItem()
|
||||
vm.actionButton = vm.actionButton.forItem(vm.episode)
|
||||
if (vm.actionButton.getLabel() != actionButton.getLabel()) actionButton = vm.actionButton
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -58,10 +58,8 @@ import android.view.ViewGroup
|
|||
import android.widget.Toast
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -69,11 +67,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -310,7 +310,8 @@ class AudioPlayerFragment : Fragment() {
|
|||
}, onValueChangeFinished = {
|
||||
Logd(TAG, "Slider onValueChangeFinished: $sliderValue")
|
||||
currentPosition = sliderValue.toInt()
|
||||
if (playbackService?.isServiceReady() == true) seekTo(currentPosition)
|
||||
// if (playbackService?.isServiceReady() == true) seekTo(currentPosition)
|
||||
seekTo(currentPosition)
|
||||
})
|
||||
Row {
|
||||
Text(DurationConverter.getDurationStringLong(currentPosition), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
|
@ -335,6 +336,45 @@ class AudioPlayerFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VolumeAdaptionDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf((currentMedia as? EpisodeMedia)?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
|
||||
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 {
|
||||
VolumeAdaptionSetting.entries.forEach { item ->
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = (item == selectedOption),
|
||||
onCheckedChange = { _ ->
|
||||
Logd(TAG, "row clicked: $item $selectedOption")
|
||||
if (item != selectedOption) {
|
||||
onOptionSelected(item)
|
||||
// currentItem = upsertBlk(currentItem!!) {
|
||||
// it.media?.volumeAdaptionSetting = item
|
||||
// }
|
||||
if (currentMedia is EpisodeMedia) {
|
||||
(currentMedia as? EpisodeMedia)?.volumeAdaptionSetting = item
|
||||
currentMedia = currentItem!!.media
|
||||
curMedia = currentMedia
|
||||
playbackService?.mPlayer?.pause(false, reinit = true)
|
||||
playbackService?.mPlayer?.resume()
|
||||
}
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(text = stringResource(item.resId), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Toolbar() {
|
||||
val media: Playable = curMedia ?: return
|
||||
|
@ -342,6 +382,8 @@ class AudioPlayerFragment : Fragment() {
|
|||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val mediaType = curMedia?.getMediaType()
|
||||
val notAudioOnly = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
|
||||
var showVolumeDialog by remember { mutableStateOf(false) }
|
||||
if (showVolumeDialog) VolumeAdaptionDialog(showVolumeDialog, onDismissRequest = { showVolumeDialog = false })
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_down), tint = textColor, contentDescription = "Collapse", modifier = Modifier.clickable {
|
||||
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
|
@ -380,6 +422,11 @@ class AudioPlayerFragment : Fragment() {
|
|||
shareDialog.show((requireActivity().supportFragmentManager), "ShareEpisodeDialog")
|
||||
}
|
||||
})
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_volume_adaption), tint = textColor, contentDescription = "Volume adaptation", modifier = Modifier.clickable {
|
||||
if (currentItem != null) {
|
||||
showVolumeDialog = true
|
||||
}
|
||||
})
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_offline_share_24), tint = textColor, contentDescription = "Share Note", modifier = Modifier.clickable {
|
||||
val notes = if (showHomeText) readerhtml else feedItem?.description
|
||||
if (!notes.isNullOrEmpty()) {
|
||||
|
@ -547,7 +594,6 @@ class AudioPlayerFragment : Fragment() {
|
|||
fun updateUi(media: Playable) {
|
||||
Logd(TAG, "updateUi called $media")
|
||||
titleText = media.getEpisodeTitle()
|
||||
onPositionUpdate(FlowEvent.PlaybackPositionEvent(media, media.getPosition(), media.getDuration()))
|
||||
if (prevMedia?.getIdentifier() != media.getIdentifier()) imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
|
||||
if (isPlayingVideoLocally && (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
|
||||
// (activity as MainActivity).bottomSheet.setLocked(true)
|
||||
|
@ -722,7 +768,7 @@ class AudioPlayerFragment : Fragment() {
|
|||
isCollapsed = false
|
||||
if (shownotesCleaner == null) shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
// showPlayer1 = false
|
||||
if (currentMedia != null) updateUi(currentMedia!!)
|
||||
// if (currentMedia != null) updateUi(currentMedia!!)
|
||||
setIsShowPlay(isShowPlay)
|
||||
updateDetails()
|
||||
// }
|
||||
|
@ -732,7 +778,7 @@ class AudioPlayerFragment : Fragment() {
|
|||
Logd(TAG, "onCollaped()")
|
||||
isCollapsed = true
|
||||
// showPlayer1 = true
|
||||
if (currentMedia != null) updateUi(currentMedia!!)
|
||||
// if (currentMedia != null) updateUi(currentMedia!!)
|
||||
setIsShowPlay(isShowPlay)
|
||||
}
|
||||
|
||||
|
@ -821,15 +867,16 @@ class AudioPlayerFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
}
|
||||
// override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// super.onCreate(savedInstanceState)
|
||||
// retainInstance = true
|
||||
// }
|
||||
|
||||
override fun onResume() {
|
||||
Logd(TAG, "onResume() isCollapsed: $isCollapsed")
|
||||
super.onResume()
|
||||
loadMediaInfo()
|
||||
if (curMedia != null) onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia!!, curMedia!!.getPosition(), curMedia!!.getDuration()))
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -844,7 +891,7 @@ class AudioPlayerFragment : Fragment() {
|
|||
//// Logd(TAG, "controllerFuture.addListener: $mediaController")
|
||||
// }, MoreExecutors.directExecutor())
|
||||
|
||||
loadMediaInfo()
|
||||
// loadMediaInfo()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -883,6 +930,7 @@ class AudioPlayerFragment : Fragment() {
|
|||
val currentitem = event.episode
|
||||
if (currentMedia?.getIdentifier() == null || currentitem.media?.getIdentifier() != currentMedia?.getIdentifier()) {
|
||||
currentMedia = currentitem.media
|
||||
updateUi(currentMedia!!)
|
||||
setItem(currentitem)
|
||||
}
|
||||
(activity as MainActivity).setPlayerVisible(true)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
|
||||
|
||||
</vector>
|
|
@ -165,10 +165,12 @@
|
|||
<string name="feed_auto_download_new">Only new items</string>
|
||||
<string name="feed_auto_download_newer">Newest unplayed</string>
|
||||
<string name="feed_auto_download_older">Oldest unplayed</string>
|
||||
<string name="feed_auto_download_soon">Marked as Soon</string>
|
||||
|
||||
<string name="put_in_queue_label">Add to queue…</string>
|
||||
<string name="remove_from_other_queues">Remove from other queues</string>
|
||||
|
||||
<string name="create_synthetic_first_note">You need to create some synthetic feeds first</string>
|
||||
<string name="remove_from_current_feed">Remove from current feed</string>
|
||||
|
||||
<string name="feed_new_episodes_action_nothing">Nothing</string>
|
||||
|
@ -223,6 +225,7 @@
|
|||
<string name="feed_delete_confirmation_msg_batch">Please confirm that you want to remove the selected podcasts, ALL their episodes (including downloaded episodes), and its statistics.</string>
|
||||
<string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\" and its statistics. The files in the local source folder will not be deleted.</string>
|
||||
<string name="feed_remover_msg">Removing podcast</string>
|
||||
<string name="not_erase_message">Only episodes from synthetic feeds can be erased.</string>
|
||||
<string name="feed_delete_reason_msg">For future reference, you can record a reason here:</string>
|
||||
<string name="load_complete_feed">Refresh complete podcast</string>
|
||||
<string name="multi_select">Multi select</string>
|
||||
|
@ -258,6 +261,7 @@
|
|||
<string name="reserve_episodes_label">Reserve episodes</string>
|
||||
<string name="null_label">Null</string>
|
||||
<string name="delete_label">Delete</string>
|
||||
<string name="delete_failed_simple">Unable to delete file.</string>
|
||||
<string name="delete_failed">Unable to delete file. Rebooting the device could help.</string>
|
||||
<string name="delete_local_failed">Unable to delete file. Try re-connecting the local folder from the podcast info screen.</string>
|
||||
<string name="delete_episode_label">Delete episode media</string>
|
||||
|
@ -280,6 +284,7 @@
|
|||
<item quantity="other">%d episodes marked as favorite.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="combo_action">Combo action</string>
|
||||
<string name="shelve_label">Shelve to synthetic</string>
|
||||
<string name="add_to_queue_label">Add to active queue</string>
|
||||
<plurals name="added_to_queue_batch_label">
|
||||
|
@ -521,6 +526,8 @@
|
|||
<string name="pref_stream_over_download_sum">Display stream button instead of download button in lists</string>
|
||||
<string name="pref_low_quality_on_mobile_title">Prefer low quality on mobile</string>
|
||||
<string name="pref_low_quality_on_mobile_sum">On metered network, only low quality media (if available) is fetched</string>
|
||||
<string name="pref_use_adaptive_progress_title">Update progress adaptively</string>
|
||||
<string name="pref_use_adaptive_progress_sum">Update the progress in an adaptive interval set as the higher of 5 seconds or 2 percent of the media duration. This saves some energy. Otherwise it\'s updated every 5 seconds.</string>
|
||||
<string name="pref_metered_network_title">Metered network settings</string>
|
||||
<string name="pref_mobileUpdate_sum">Select what should be allowed over the mobile data connection</string>
|
||||
<string name="pref_mobileUpdate_refresh">Podcast refresh</string>
|
||||
|
|
|
@ -66,6 +66,11 @@
|
|||
android:key="prefLowQualityOnMobile"
|
||||
android:summary="@string/pref_low_quality_on_mobile_sum"
|
||||
android:title="@string/pref_low_quality_on_mobile_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="prefUseAdaptiveProgressUpdate"
|
||||
android:summary="@string/pref_use_adaptive_progress_sum"
|
||||
android:title="@string/pref_use_adaptive_progress_title"/>
|
||||
<Preference
|
||||
android:title="@string/pref_playback_video_mode"
|
||||
android:key="prefPlaybackVideoModeLauncher"
|
||||
|
|
15
changelog.md
15
changelog.md
|
@ -1,3 +1,18 @@
|
|||
# 6.13.0
|
||||
|
||||
* updates playback position adaptively (in app and in widget) in an interval being the longer of 5 seconds and 2 percent of the media duration
|
||||
* this can be enabled/disabled in Settings->Playback->"Update progress adaptively", default to true
|
||||
* unadaptive interval, same as the previous, is 2 seconds
|
||||
* added volume adaptation control to player detailed view to set for current media and it takes precedence over that in feed settings
|
||||
* tuned the AudioPlayer fragment
|
||||
* added a few new actions to swipe, bring it essentially equivalent to multi-select menus
|
||||
* added auto-download policy: "Marked as Soon"
|
||||
* when deleting media file, set the playState to Skipped only if the current state is lower than Skipped
|
||||
* during cast to speaker (in the Play app), tap on the position bar in the PlayerUI changes the position
|
||||
* avoided the snack message "can't delete file ..." after streaming an episode
|
||||
* fixed (again, sorry) the action button not updating issue after download in episodes lists
|
||||
* google cast framework is updated to 22.0 (in the Play apk)
|
||||
|
||||
# 6.12.8
|
||||
|
||||
* set episode's playState to Skipped when its media file is removed
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
Version 6.13.0
|
||||
|
||||
* updates playback position adaptively (in app and in widget) in an interval being the longer of 5 seconds and 2 percent of the media duration
|
||||
* this can be enabled/disabled in Settings->Playback->"Update progress adaptively", default to true
|
||||
* unadaptive interval, same as the previous, is 2 seconds
|
||||
* added volume adaptation control to player detailed view to set for current media and it takes precedence over that in feed settings
|
||||
* added a few new actions to swipe, bring it essentially equivalent to multi-select menus
|
||||
* added auto-download policy: "Marked as Soon"
|
||||
* when deleting media file, set the playState to Skipped only if the current state is lower than Skipped
|
||||
* avoided the snack message "can't delete file ..." after streaming an episode
|
||||
* fixed (again, sorry) the action button not updating issue after download in episodes lists
|
||||
* tuned the AudioPlayer fragment
|
|
@ -52,7 +52,7 @@ okhttpUrlconnection = "4.12.0"
|
|||
okio = "3.9.0"
|
||||
paletteKtx = "1.0.0"
|
||||
playServicesBase = "18.5.0"
|
||||
playServicesCastFramework = "21.5.0"
|
||||
playServicesCastFramework = "22.0.0"
|
||||
preferenceKtx = "1.2.1"
|
||||
readability4j = "1.0.8"
|
||||
recyclerview = "1.3.2"
|
||||
|
|
Loading…
Reference in New Issue