6.13.0 commit

This commit is contained in:
Xilin Jia 2024-10-29 19:13:57 +01:00
parent 38a27080a4
commit 69de20fd35
22 changed files with 374 additions and 218 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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