6.14.4 commit

This commit is contained in:
Xilin Jia 2024-11-21 19:40:56 +01:00
parent d22f29d4be
commit ddc0b09a38
32 changed files with 655 additions and 1157 deletions

View File

@ -73,6 +73,10 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* added setting to play audio only for video feeds,
* an added benefit for setting it enables Youtube media to only stream audio content, saving bandwidth.
* this differs from switching to "Audio only" on each episode, in which case, video is also streamed
* RSS feeds with no playable media can be subscribed and read/listened (via TTS)
* there are two ways to access TTS: from the action bar of EpisodeHome view, and on the list of FeedEspiosdes view
* the former plays the TTS instantly on the text available, and regardless of whether the episode as playable media or not, and the app can't control the playing except for play/pause
* the latter, only available when the episode has no media (plain RSS), does not play anything, instead, it constructs an audio file (like download) to be played as a normal media and the speed/rewind/forward can be controlled in Podcini
### Episode
@ -86,10 +90,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* in EpisodeInfo view, one can enter personal comments/notes under "My opinion" for the episode
* New episode home view with two display modes: webpage or reader
* In episode, in addition to "description" there is a new "transcript" field to save text (if any) fetched from the episode's website
* RSS feeds with no playable media can be subscribed and read/listened (via TTS)
* there are two ways to access TTS: from the action bar of EpisodeHome view, and on the list of FeedEspiosdes view
* the former plays the TTS instantly on the text available, and regardless of whether the episode as playable media or not, and the speed is not controlled in the app
* the latter, only available when the episode has no media (plain RSS), constructs an audio file (like download) to be played as normal media file and the speed can be controlled in Podcini
### Podcast/Episode list
@ -97,6 +97,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* An all new sorting dialog and mechanism for Subscriptions based on title, date, and count combinable with other criteria
* An all new way of filtering for both podcasts and episodes with expanded criteria
* in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view)
* FeedEpisodes has the option to show larger image on the list by changing the "Use wide layout" setting of the feed
* Episodes list is shown in views of Queues, Downloads, All episodes, FeedEpisodes
* New and efficient ways of click and long-click operations on both podcast and episode lists:
* click on title area opens the podcast/episode

View File

@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []
versionCode 3020302
versionName "6.14.3"
versionCode 3020303
versionName "6.14.4"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -515,6 +515,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
var playbackSpeed = FeedPreferences.SPEED_USE_GLOBAL
if (media != null) {
playbackSpeed = curState.curTempSpeed
// TODO: something strange here?
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) {
val prefs_ = media.episodeOrFetch()?.feed?.preferences
if (prefs_ != null) playbackSpeed = prefs_.playSpeed

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.preferences.fragments
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.EditTextDialogBinding
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.prefPlaybackSpeed
import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UsageStatistics.doNotAskAgain
import ac.mdiq.podcini.preferences.UserPreferences
@ -10,46 +10,55 @@ import ac.mdiq.podcini.preferences.UserPreferences.setVideoMode
import ac.mdiq.podcini.preferences.UserPreferences.speedforwardSpeed
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.PlaybackSpeedDialog
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.InputType
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.collection.ArrayMap
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.ref.WeakReference
import kotlin.math.round
class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_playback)
setupPlaybackScreen()
// buildSmartMarkAsPlayedPreference()
}
override fun onStart() {
super.onStart()
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.playback_pref)
(activity as PreferenceActivity).supportActionBar?.setTitle(R.string.playback_pref)
}
private fun setupPlaybackScreen() {
findPreference<Preference>(Prefs.prefPlaybackSpeedLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
VariableSpeedDialog.newInstance(booleanArrayOf(false, false, true),2)?.show(childFragmentManager, null)
findPreference<Preference>(Prefs.prefPlaybackSpeedLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val composeView = ComposeView(requireContext()).apply {
setContent {
val showDialog = remember { mutableStateOf(true) }
CustomTheme(requireContext()) {
PlaybackSpeedDialog(listOf(), initSpeed = prefPlaybackSpeed, maxSpeed = 3f, isGlobal = true, onDismiss = {
showDialog.value = false
(view as? ViewGroup)?.removeView(this@apply)
}) { speed -> UserPreferences.setPlaybackSpeed(speed) }
}
}
}
(view as? ViewGroup)?.addView(composeView)
true
}
findPreference<Preference>(Prefs.prefPlaybackRewindDeltaLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findPreference<Preference>(Prefs.prefPlaybackRewindDeltaLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true
}
@ -58,19 +67,55 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
true
}
findPreference<Preference>(Prefs.prefPlaybackSpeedForwardLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
EditForwardSpeedDialog(requireActivity()).show()
findPreference<Preference>(Prefs.prefPlaybackSpeedForwardLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val composeView = ComposeView(requireContext()).apply {
setContent {
val showDialog = remember { mutableStateOf(true) }
CustomTheme(requireContext()) {
PlaybackSpeedDialog(listOf(), initSpeed = speedforwardSpeed, maxSpeed = 10f, isGlobal = true, onDismiss = {
showDialog.value = false
(view as? ViewGroup)?.removeView(this@apply)
}) { speed ->
val speed_ = when {
speed < 0.0f -> 0.0f
speed > 10.0f -> 10.0f
else -> 0f
}
speedforwardSpeed = round(10 * speed_) / 10
}
}
}
}
(view as? ViewGroup)?.addView(composeView)
true
}
findPreference<Preference>(Prefs.prefPlaybackFallbackSpeedLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
EditFallbackSpeedDialog(requireActivity()).show()
findPreference<Preference>(Prefs.prefPlaybackFallbackSpeedLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val composeView = ComposeView(requireContext()).apply {
setContent {
val showDialog = remember { mutableStateOf(true) }
CustomTheme(requireContext()) {
PlaybackSpeedDialog(listOf(), initSpeed = fallbackSpeed, maxSpeed = 3f, isGlobal = true, onDismiss = {
showDialog.value = false
(view as? ViewGroup)?.removeView(this@apply)
}) { speed ->
val speed_ = when {
speed < 0.0f -> 0.0f
speed > 3.0f -> 3.0f
else -> 0f
}
fallbackSpeed = round(100 * speed_) / 100f
}
}
}
}
(view as? ViewGroup)?.addView(composeView)
true
}
findPreference<Preference>(Prefs.prefPlaybackFastForwardDeltaLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findPreference<Preference>(Prefs.prefPlaybackFastForwardDeltaLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
true
}
findPreference<Preference>(Prefs.prefStreamOverDownload.name)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? ->
findPreference<Preference>(Prefs.prefStreamOverDownload.name)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? ->
// Update all visible lists to reflect new streaming action button
// TODO: need another event type?
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent())
@ -79,8 +124,8 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
true
}
if (Build.VERSION.SDK_INT >= 31) {
findPreference<Preference>(UserPreferences.Prefs.prefUnpauseOnHeadsetReconnect.name)!!.isVisible = false
findPreference<Preference>(UserPreferences.Prefs.prefUnpauseOnBluetoothReconnect.name)!!.isVisible = false
findPreference<Preference>(UserPreferences.Prefs.prefUnpauseOnHeadsetReconnect.name)?.isVisible = false
findPreference<Preference>(UserPreferences.Prefs.prefUnpauseOnBluetoothReconnect.name)?.isVisible = false
}
buildEnqueueLocationPreference()
}
@ -109,57 +154,6 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
// Possibly put it to a common method in abstract base class
return findPreference<T>(key) ?: throw IllegalArgumentException("Preference with key '$key' is not found")
}
class EditFallbackSpeedDialog(activity: Activity) {
val TAG = this::class.simpleName ?: "Anonymous"
private val activityRef = WeakReference(activity)
fun show() {
val activity = activityRef.get() ?: return
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
binding.editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
binding.editText.text = Editable.Factory.getInstance().newEditable(fallbackSpeed.toString())
MaterialAlertDialogBuilder(activity)
.setView(binding.root)
.setTitle(R.string.edit_fallback_speed)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
var speed = binding.editText.text.toString().toFloatOrNull() ?: 0.0f
when {
speed < 0.0f -> speed = 0.0f
speed > 3.0f -> speed = 3.0f
}
fallbackSpeed = round(100 * speed) / 100
}
.setNegativeButton(R.string.cancel_label, null)
.show()
}
}
class EditForwardSpeedDialog(activity: Activity) {
val TAG = this::class.simpleName ?: "Anonymous"
private val activityRef = WeakReference(activity)
fun show() {
val activity = activityRef.get() ?: return
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
binding.editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
binding.editText.text = Editable.Factory.getInstance().newEditable(speedforwardSpeed.toString())
MaterialAlertDialogBuilder(activity)
.setView(binding.root)
.setTitle(R.string.edit_fast_forward_speed)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
var speed = binding.editText.text.toString().toFloatOrNull() ?: 0.0f
when {
speed < 0.0f -> speed = 0.0f
speed > 10.0f -> speed = 10.0f
}
speedforwardSpeed = round(10 * speed) / 10
}
.setNegativeButton(R.string.cancel_label, null)
.show()
}
}
object VideoModeDialog {
fun showDialog(context: Context) {

View File

@ -40,7 +40,7 @@ object RealmDB {
SubscriptionLog::class,
Chapter::class))
.name("Podcini.realm")
.schemaVersion(33)
.schemaVersion(34)
.migration({ mContext ->
val oldRealm = mContext.oldRealm // old realm using the previous schema
val newRealm = mContext.newRealm // new realm using the new schema

View File

@ -15,6 +15,8 @@ class FeedPreferences : EmbeddedRealmObject {
var feedID: Long = 0L
var useWideLayout: Boolean = false
/**
* @return true if this feed should be refreshed when everything else is being refreshed
* if false the feed should only be refreshed if requested directly.

View File

@ -1,24 +1,32 @@
package ac.mdiq.podcini.ui.activity
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTranslucentTheme
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog
import android.os.Bundle
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
// This is for widget
class PlaybackSpeedDialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getTranslucentTheme(this))
super.onCreate(savedInstanceState)
val speedDialog: VariableSpeedDialog? = VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), 2)
speedDialog?.show(supportFragmentManager, null)
}
class InnerVariableSpeedDialog : VariableSpeedDialog() {
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
requireActivity().finish()
val composeView = ComposeView(this).apply {
setContent {
var showSpeedDialog by remember { mutableStateOf(true) }
if (showSpeedDialog) PlaybackSpeedFullDialog(settingCode = booleanArrayOf(true, true, true), indexDefault = 0, maxSpeed = 3f,
onDismiss = {
showSpeedDialog = false
(parent as? ViewGroup)?.removeView(this)
finish()
})
}
}
(window.decorView as? ViewGroup)?.addView(composeView)
}
}

View File

@ -34,6 +34,7 @@ import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.compose.ChaptersDialog
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog
import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
@ -65,8 +66,10 @@ import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
import androidx.fragment.app.DialogFragment
@ -315,16 +318,6 @@ class VideoplayerActivity : CastEnabledActivity() {
val media = curMedia ?: return false
val feedItem = (media as? EpisodeMedia)?.episodeOrFetch()
when {
// item.itemId == R.id.add_to_favorites_item && feedItem != null -> {
// setFavorite(feedItem, true)
// videoEpisodeFragment.isFavorite = true
// invalidateOptionsMenu()
// }
// item.itemId == R.id.remove_from_favorites_item && feedItem != null -> {
// setFavorite(feedItem, false)
// videoEpisodeFragment.isFavorite = false
// invalidateOptionsMenu()
// }
item.itemId == R.id.disable_sleeptimer_item || item.itemId == R.id.set_sleeptimer_item ->
SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
item.itemId == R.id.audio_controls -> {
@ -343,8 +336,20 @@ class VideoplayerActivity : CastEnabledActivity() {
val shareDialog = ShareDialog.newInstance(feedItem)
shareDialog.show(supportFragmentManager, "ShareEpisodeDialog")
}
item.itemId == R.id.playback_speed ->
VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true))?.show(supportFragmentManager, null)
item.itemId == R.id.playback_speed -> {
val composeView = ComposeView(this).apply {
setContent {
var showSpeedDialog by remember { mutableStateOf(true) }
if (showSpeedDialog) PlaybackSpeedFullDialog(settingCode = booleanArrayOf(true, true, true), indexDefault = 0, maxSpeed = 3f,
onDismiss = {
showSpeedDialog = false
(parent as? ViewGroup)?.removeView(this)
})
}
}
(window.decorView as? ViewGroup)?.addView(composeView)
// VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true))?.show(supportFragmentManager, null)
}
else -> return false
}
return true

View File

@ -1,6 +1,7 @@
package ac.mdiq.podcini.ui.compose
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@ -18,8 +19,17 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
@ -30,6 +40,8 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.delay
import kotlin.math.cos
import kotlin.math.sin
@Composable
private fun CustomTextField(

View File

@ -33,8 +33,6 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
import ac.mdiq.podcini.storage.model.Feed.Companion.newId
import ac.mdiq.podcini.storage.model.PlayState.Companion.fromCode
import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
@ -48,6 +46,7 @@ import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
import ac.mdiq.vista.extractor.Vista
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
@ -94,7 +93,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.documentfile.provider.DocumentFile
import coil.compose.AsyncImage
import coil.request.CachePolicy
@ -110,9 +108,10 @@ import kotlin.math.roundToInt
@Composable
fun InforBar(text: MutableState<String>, leftAction: MutableState<SwipeAction>, rightAction: MutableState<SwipeAction>, actionConfig: () -> Unit) {
val textColor = MaterialTheme.colorScheme.onSurface
val buttonColor = MaterialTheme.colorScheme.tertiary
Logd("InforBar", "textState: ${text.value}")
Row {
Icon(imageVector = ImageVector.vectorResource(leftAction.value.getActionIcon()), tint = textColor, contentDescription = "left_action_icon",
Icon(imageVector = ImageVector.vectorResource(leftAction.value.getActionIcon()), tint = buttonColor, contentDescription = "left_action_icon",
modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig))
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_left_alt_24), tint = textColor, contentDescription = "left_arrow",
modifier = Modifier.width(24.dp).height(24.dp))
@ -121,13 +120,11 @@ fun InforBar(text: MutableState<String>, leftAction: MutableState<SwipeAction>,
Spacer(modifier = Modifier.weight(1f))
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_right_alt_24), tint = textColor, contentDescription = "right_arrow",
modifier = Modifier.width(24.dp).height(24.dp))
Icon(imageVector = ImageVector.vectorResource(rightAction.value.getActionIcon()), tint = textColor, contentDescription = "right_action_icon",
Icon(imageVector = ImageVector.vectorResource(rightAction.value.getActionIcon()), tint = buttonColor, contentDescription = "right_action_icon",
modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig))
}
}
//var queueChanged by mutableIntStateOf(0)
@Stable
class EpisodeVM(var episode: Episode) {
var positionState by mutableStateOf(episode.media?.position?:0)
@ -137,12 +134,11 @@ class EpisodeVM(var episode: Episode) {
var ratingState by mutableIntStateOf(episode.rating)
var inProgressState by mutableStateOf(episode.isInProgress)
var downloadState by mutableIntStateOf(if (episode.media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
// var actionButton by mutableStateOf(forItem(episode))
var viewCount by mutableIntStateOf(episode.viewCount)
var actionButton by mutableStateOf<EpisodeActionButton>(NullActionButton(episode))
var actionRes by mutableIntStateOf(actionButton.getDrawable())
var showAltActionsDialog by mutableStateOf(false)
var dlPercent by mutableIntStateOf(0)
// var inQueueState by mutableStateOf(curQueue.contains(episode))
var isSelected by mutableStateOf(false)
var prog by mutableFloatStateOf(0f)
@ -442,7 +438,7 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed: Feed? = null,
fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed: Feed? = null, layoutMode: Int = 0,
isDraggable: Boolean = false, dragCB: ((Int, Int)->Unit)? = null,
refreshCB: (()->Unit)? = null, leftSwipeCB: ((Episode) -> Unit)? = null, rightSwipeCB: ((Episode) -> Unit)? = null,
actionButton_: ((Episode)-> EpisodeActionButton)? = null) {
@ -622,21 +618,88 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
}
}
fun toggleSelected(vm: EpisodeVM) {
vm.isSelected = !vm.isSelected
if (vm.isSelected) selected.add(vm.episode)
else selected.remove(vm.episode)
}
val titleMaxLines = if (layoutMode == 0) 2 else 3
@Composable
fun TitleColumn(vm: EpisodeVM, index: Int, modifier: Modifier) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier.padding(start = 6.dp, end = 6.dp)
.combinedClickable(onClick = {
Logd(TAG, "clicked: ${vm.episode.title}")
if (selectMode) toggleSelected(vm)
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
}, onLongClick = {
selectMode = !selectMode
vm.isSelected = selectMode
selected.clear()
if (selectMode) {
selected.add(vms[index].episode)
longPressIndex = index
} else {
selectedSize = 0
longPressIndex = -1
}
Logd(TAG, "long clicked: ${vm.episode.title}")
})) {
Text(vm.episode.title ?: "", color = textColor, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, maxLines = titleMaxLines, overflow = TextOverflow.Ellipsis)
if (layoutMode == 0) {
Row(verticalAlignment = Alignment.CenterVertically) {
val playStateRes = PlayState.fromCode(vm.playedState).res
Icon(imageVector = ImageVector.vectorResource(playStateRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "playState", modifier = Modifier.background(Color.Green.copy(alpha = 0.6f)).width(16.dp).height(16.dp))
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(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(16.dp).height(16.dp))
val curContext = LocalContext.current
val dateSizeText = " · " + formatDateTimeFlex(vm.episode.getPubDate()) +
if (vm.viewCount > 0) " · " + formatNumber(vm.viewCount) else "" +
" · " + getDurationStringLong(vm.durationState) + " · " +
if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
val playStateRes = PlayState.fromCode(vm.playedState).res
Icon(imageVector = ImageVector.vectorResource(playStateRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "playState", modifier = Modifier.background(Color.Green.copy(alpha = 0.6f)).width(16.dp).height(16.dp))
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(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(16.dp).height(16.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
val curContext = LocalContext.current
val dateSizeText = formatDateTimeFlex(vm.episode.getPubDate()) +
if (vm.viewCount > 0) " · " + formatNumber(vm.viewCount) + " views" else "" +
" · " + getDurationStringLong(vm.durationState) + " · " +
if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
}
@Composable
fun MainRow(vm: EpisodeVM, index: Int, isBeingDragged: Boolean, yOffset: Float, onDragStart: () -> Unit, onDrag: (Float) -> Unit, onDragEnd: () -> Unit) {
val textColor = MaterialTheme.colorScheme.onSurface
fun toggleSelected() {
vm.isSelected = !vm.isSelected
if (vm.isSelected) selected.add(vms[index].episode)
else selected.remove(vms[index].episode)
}
val buttonColor = MaterialTheme.colorScheme.tertiary
val density = LocalDensity.current
val imageWidth = if (layoutMode == 0) 56.dp else 150.dp
val imageHeight = if (layoutMode == 0) 56.dp else 100.dp
Row(Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
.offset(y = with(density) { yOffset.toDp() })) {
if (isDraggable) {
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
Icon(imageVector = ImageVector.vectorResource(typedValue.resourceId), tint = textColor, contentDescription = "drag handle",
Icon(imageVector = ImageVector.vectorResource(typedValue.resourceId), tint = buttonColor, contentDescription = "drag handle",
modifier = Modifier.width(50.dp).align(Alignment.CenterVertically).padding(start = 10.dp, end = 15.dp)
.draggable(orientation = Orientation.Vertical,
state = rememberDraggableState { delta -> onDrag(delta) },
@ -644,116 +707,53 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
onDragStopped = { onDragEnd() }
))
}
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs()
val imgLoc = remember(vm) { ImageResourceUtils.getEpisodeListImageLocation(vm.episode) }
// Logd(TAG, "imgLoc: $imgLoc")
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
contentDescription = "imgvCover",
modifier = Modifier.width(56.dp).height(56.dp)
.constrainAs(imgvCover) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
val imgLoc = remember(vm) { ImageResourceUtils.getEpisodeListImageLocation(vm.episode) }
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
contentDescription = "imgvCover",
modifier = Modifier.width(imageWidth).height(imageHeight)
.clickable(onClick = {
Logd(TAG, "icon clicked!")
if (selectMode) toggleSelected(vm)
else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
}))
Box {
TitleColumn(vm, index, modifier = Modifier.fillMaxWidth())
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
}
if (actionButton_ == null) {
LaunchedEffect(key1 = status, key2 = vm.downloadState) {
if (index >= vms.size) return@LaunchedEffect
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media?.downloadUrl ?: "") ?: 0
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vm.episode.playState} ${vms[index].episode.title}")
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
vm.actionButton = vm.actionButton.forItem(vm.episode)
if (vm.actionButton.getLabel() != actionButton.getLabel()) {
actionButton = vm.actionButton
}
.clickable(onClick = {
Logd(TAG, "icon clicked!")
if (selectMode) toggleSelected()
else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
}))
if (vm.playedState >= PlayState.SKIPPED.code) {
Icon(imageVector = ImageVector.vectorResource(fromCode(vm.playedState).res), tint = textColor, contentDescription = "play state",
modifier = Modifier.background(Color.Green.copy(alpha = 0.6f)).width(20.dp).height(20.dp)
.constrainAs(checkMark) {
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
})
}
}
Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp)
.combinedClickable(onClick = {
Logd(TAG, "clicked: ${vm.episode.title}")
if (selectMode) toggleSelected()
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
}, onLongClick = {
selectMode = !selectMode
vm.isSelected = selectMode
selected.clear()
if (selectMode) {
selected.add(vms[index].episode)
longPressIndex = index
} else {
selectedSize = 0
longPressIndex = -1
}
Logd(TAG, "long clicked: ${vm.episode.title}")
})) {
// LaunchedEffect(key1 = queueChanged) {
// if (index >= vms.size) return@LaunchedEffect
// vms[index].inQueueState = curQueue.contains(vms[index].episode)
// }
Row(verticalAlignment = Alignment.CenterVertically) {
// 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(16.dp).height(16.dp))
val playStateRes = PlayState.fromCode(vm.playedState).res
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(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(16.dp).height(16.dp))
val curContext = LocalContext.current
// val dur = remember { vm.episode.media?.getDuration() ?: 0 }
// val durText = DurationConverter.getDurationStringLong(vm.durationState)
val dateSizeText = " · " + formatDateTimeFlex(vm.episode.getPubDate()) + " · " + getDurationStringLong(vm.durationState) + " · " +
if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(vm.episode.title ?: "", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
}
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
}
if (actionButton_ == null) {
LaunchedEffect(key1 = status, key2 = vm.downloadState) {
if (index >= vms.size) return@LaunchedEffect
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media?.downloadUrl ?: "") ?: 0
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vm.episode.playState} ${vms[index].episode.title}")
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
vm.actionButton = vm.actionButton.forItem(vm.episode)
if (vm.actionButton.getLabel() != actionButton.getLabel()) {
} else {
LaunchedEffect(Unit) {
Logd(TAG, "LaunchedEffect init actionButton")
vm.actionButton = actionButton_(vm.episode)
actionButton = vm.actionButton
}
}
} else {
LaunchedEffect(Unit) {
Logd(TAG, "LaunchedEffect init actionButton")
vm.actionButton = actionButton_(vm.episode)
actionButton = vm.actionButton
// vm.actionRes = vm.actionButton!!.getDrawable()
Box(contentAlignment = Alignment.Center, modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.BottomEnd)
.pointerInput(Unit) {
detectTapGestures(onLongPress = { vms[index].showAltActionsDialog = true }, onTap = { actionButton.onClick(activity) })
}) {
vm.actionRes = actionButton.getDrawable()
Icon(imageVector = ImageVector.vectorResource(vm.actionRes), tint = buttonColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent },
strokeWidth = 4.dp, color = buttonColor, modifier = Modifier.width(33.dp).height(37.dp))
if (actionButton.processing > -1) CircularProgressIndicator(progress = { 0.01f * actionButton.processing },
strokeWidth = 4.dp, color = buttonColor, modifier = Modifier.width(33.dp).height(37.dp))
}
if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, onDismiss = { vm.showAltActionsDialog = false })
}
Box(contentAlignment = Alignment.Center, modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.CenterVertically)
.pointerInput(Unit) {
detectTapGestures(onLongPress = { vms[index].showAltActionsDialog = true },
onTap = {
// vms[index].actionButton.onClick(activity)
actionButton.onClick(activity)
})
}, ) {
// Logd(TAG, "button box")
vm.actionRes = actionButton.getDrawable()
Icon(imageVector = ImageVector.vectorResource(vm.actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent },
strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(33.dp).height(37.dp))
if (actionButton.processing > -1) CircularProgressIndicator(progress = { 0.01f * actionButton.processing },
strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(33.dp).height(37.dp))
}
if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, onDismiss = { vm.showAltActionsDialog = false })
}
}
@ -763,11 +763,11 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
if (vm.inProgressState || InTheatre.isCurMedia(vm.episode.media)) {
val pos = vm.positionState
val dur = remember { vm.episode.media?.getDuration() ?: 0 }
val durText = remember { DurationConverter.getDurationStringLong(dur) }
val durText = remember { getDurationStringLong(dur) }
vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
// Logd(TAG, "$index vm.prog: ${vm.prog}")
Row {
Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall)
Text(getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall)
LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
Text(durText, color = textColor, style = MaterialTheme.typography.bodySmall)
}
@ -851,9 +851,10 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
}
}
if (selectMode) {
val buttonColor = MaterialTheme.colorScheme.tertiary
Row(modifier = Modifier.align(Alignment.TopEnd).width(150.dp).height(45.dp)
.background(Color.LightGray), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_upward_24), tint = Color.Black, contentDescription = null,
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_upward_24), tint = buttonColor, contentDescription = null,
modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp)
.clickable(onClick = {
selected.clear()
@ -861,7 +862,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
selectedSize = selected.size
Logd(TAG, "selectedIds: ${selected.size}")
}))
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_downward_24), tint = Color.Black, contentDescription = null,
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_downward_24), tint = buttonColor, contentDescription = null,
modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp)
.clickable(onClick = {
selected.clear()
@ -870,7 +871,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
Logd(TAG, "selectedIds: ${selected.size}")
}))
var selectAllRes by remember { mutableIntStateOf(R.drawable.ic_select_all) }
Icon(imageVector = ImageVector.vectorResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)
Icon(imageVector = ImageVector.vectorResource(selectAllRes), tint = buttonColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)
.clickable(onClick = {
if (selectedSize != vms.size) {
selected.clear()
@ -951,6 +952,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
val buttonColor = MaterialTheme.colorScheme.tertiary
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
var selectNone by remember { mutableStateOf(false) }
@ -1012,7 +1014,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
var lowerSelected by remember { mutableStateOf(false) }
var higherSelected by remember { mutableStateOf(false) }
Spacer(Modifier.weight(1f))
if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else buttonColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val hIndex = selectedList.indexOfLast { it.value }
if (hIndex < 0) return@clickable
if (!lowerSelected) {
@ -1029,7 +1031,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
if (expandRow) Text("X", color = textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
if (expandRow) Text("X", color = buttonColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
lowerSelected = false
higherSelected = false
for (i in item.values.indices) {
@ -1039,7 +1041,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
if (expandRow) Text(">>>", color = if (higherSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
if (expandRow) Text(">>>", color = if (higherSelected) Color.Green else buttonColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val lIndex = selectedList.indexOfFirst { it.value }
if (lIndex < 0) return@clickable
if (!higherSelected) {

View File

@ -3,17 +3,24 @@ package ac.mdiq.podcini.ui.compose
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.feed.FeedBuilder
import ac.mdiq.podcini.net.feed.searcher.PodcastSearchResult
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.prefPlaybackSpeed
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.storage.database.Feeds.buildTags
import ac.mdiq.podcini.storage.database.Feeds.createSynthetic
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
import ac.mdiq.podcini.storage.database.Feeds.getTags
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.storage.model.SubscriptionLog
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment
import ac.mdiq.podcini.ui.fragment.OnlineFeedFragment
@ -22,6 +29,7 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter
import android.util.Log
import android.view.Gravity
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@ -29,16 +37,17 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
@ -49,14 +58,20 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
import kotlin.math.round
@Composable
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
@ -154,7 +169,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
val url = feed.feedUrl
if (feedBuilder.isYoutube(url)) {
if (feedBuilder.isYoutubeChannel()) {
val nTabs = feedBuilder.youtubeChannelValidTabs()
// val nTabs = feedBuilder.youtubeChannelValidTabs()
feedBuilder.buildYTChannel(0, "") { feed, _ -> feedBuilder.subscribe(feed) }
// if (nTabs > 1) showYTChannelDialog = true
// else feedBuilder.buildYTChannel(0, "") { feed, map -> feedBuilder.subscribe(feed) }
@ -188,25 +203,17 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
val textColor = MaterialTheme.colorScheme.onSurface
Text(feed.title, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(bottom = 4.dp))
Row {
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs()
Box(modifier = Modifier.width(56.dp).height(56.dp)) {
val imgLoc = remember(feed) { feed.imageUrl }
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), contentDescription = "imgvCover",
modifier = Modifier.width(65.dp).height(65.dp).constrainAs(imgvCover) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
})
modifier = Modifier.fillMaxSize())
if (feed.feedId > 0 || log != null) {
Logd("OnlineFeedItem", "${feed.feedId} $log")
val alpha = 1.0f
val iRes = if (feed.feedId > 0) R.drawable.ic_check else R.drawable.baseline_clear_24
Icon(imageVector = ImageVector.vectorResource(iRes), tint = textColor, contentDescription = "played_mark",
modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
})
modifier = Modifier.background(Color.Green).alpha(alpha).align(Alignment.BottomEnd))
}
}
Column(Modifier.padding(start = 10.dp)) {
@ -325,6 +332,8 @@ fun TagSettingDialog(feeds: List<Feed>, onDismiss: () -> Unit) {
}
}
Row(Modifier.padding(start = 20.dp, end = 20.dp, top = 10.dp)) {
Button(onClick = { onDismiss() }) { Text("Cancel") }
Spacer(Modifier.weight(1f))
Button(onClick = {
if ((tags.toSet() + commonTags.toSet()).isNotEmpty()) for (f in feeds) upsertBlk(f) {
if (commonTags.isNotEmpty()) it.preferences?.tags?.removeAll(commonTags)
@ -333,8 +342,232 @@ fun TagSettingDialog(feeds: List<Feed>, onDismiss: () -> Unit) {
buildTags()
onDismiss()
}) { Text("Confirm") }
Spacer(Modifier.weight(1f))
Button(onClick = { onDismiss() }) { Text("Cancel") }
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlaybackSpeedDialog(feeds: List<Feed>, initSpeed: Float, maxSpeed: Float, isGlobal: Boolean = false, onDismiss: () -> Unit, speedCB: (Float) -> Unit) {
// min speed set to 0.1 and max speed at 10
fun speed2Slider(speed: Float): Float {
return if (speed < 1) (speed - 0.1f) / 1.8f else (speed - 2f + maxSpeed) / 2 / (maxSpeed - 1f)
}
fun slider2Speed(slider: Float): Float {
return if (slider < 0.5) 1.8f * slider + 0.1f else 2 * (maxSpeed - 1f) * slider + 2f - maxSpeed
}
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = onDismiss) {
Card(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column {
Text(stringResource(R.string.playback_speed), fontSize = MaterialTheme.typography.headlineSmall.fontSize, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 4.dp))
// var speed by remember { mutableStateOf(if (isGlobal) prefPlaybackSpeed else if (feeds.size == 1) feeds[0].preferences!!.playSpeed else 1f) }
var speed by remember { mutableStateOf(initSpeed) }
var sliderPosition by remember { mutableFloatStateOf(speed2Slider(if (speed == FeedPreferences.SPEED_USE_GLOBAL) 1f else speed)) }
var useGlobal by remember { mutableStateOf(!isGlobal && speed == FeedPreferences.SPEED_USE_GLOBAL) }
if (!isGlobal) Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = useGlobal, onCheckedChange = { isChecked ->
useGlobal = isChecked
speed = if (useGlobal) FeedPreferences.SPEED_USE_GLOBAL
else if (feeds.size == 1) {
if (feeds[0].preferences!!.playSpeed == FeedPreferences.SPEED_USE_GLOBAL) prefPlaybackSpeed
else feeds[0].preferences!!.playSpeed
} else 1f
if (!useGlobal) sliderPosition = speed2Slider(speed)
})
Text(stringResource(R.string.global_default))
}
if (!useGlobal) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Text(text = String.format(Locale.getDefault(), "%.2fx", speed))
}
val stepSize = 0.05f
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Text("-", fontSize = MaterialTheme.typography.headlineLarge.fontSize, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable(onClick = {
val speed_ = round(speed / stepSize) * stepSize - stepSize
if (speed_ >= 0.1f) {
speed = speed_
sliderPosition = speed2Slider(speed)
}
}))
Slider(value = sliderPosition, modifier = Modifier.weight(1f).height(5.dp).padding(start = 20.dp, end = 20.dp),
onValueChange = {
sliderPosition = it
speed = slider2Speed(sliderPosition)
Logd("PlaybackSpeedDialog", "slider value: $it $speed}")
})
Text("+", fontSize = MaterialTheme.typography.headlineLarge.fontSize, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable(onClick = {
val speed_ = round(speed / stepSize) * stepSize + stepSize
if (speed_ <= maxSpeed) {
speed = speed_
sliderPosition = speed2Slider(speed)
}
}))
}
}
Row(Modifier.padding(start = 20.dp, end = 20.dp, top = 10.dp)) {
Button(onClick = { onDismiss() }) { Text("Cancel") }
Spacer(Modifier.weight(1f))
Button(onClick = {
val newSpeed = if (useGlobal) FeedPreferences.SPEED_USE_GLOBAL else speed
speedCB(newSpeed)
onDismiss()
}) { Text("Confirm") }
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun PlaybackSpeedFullDialog(settingCode: BooleanArray, indexDefault: Int, maxSpeed: Float, onDismiss: () -> Unit) {
val TAG = "PlaybackSpeedFullDialog"
// min speed set to 0.1 and max speed at 10
fun speed2Slider(speed: Float): Float {
return if (speed < 1) (speed - 0.1f) / 1.8f else (speed - 2f + maxSpeed) / 2 / (maxSpeed - 1f)
}
fun slider2Speed(slider: Float): Float {
return if (slider < 0.5) 1.8f * slider + 0.1f else 2 * (maxSpeed - 1f) * slider + 2f - maxSpeed
}
fun readPlaybackSpeedArray(valueFromPrefs: String?): List<Float> {
if (valueFromPrefs != null) {
try {
val jsonArray = JSONArray(valueFromPrefs)
val selectedSpeeds: MutableList<Float> = ArrayList()
for (i in 0 until jsonArray.length()) selectedSpeeds.add(jsonArray.getDouble(i).toFloat())
return selectedSpeeds
} catch (e: JSONException) {
Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray")
e.printStackTrace()
}
}
return mutableListOf(1.0f, 1.25f, 1.5f)
}
// fun getPlaybackSpeedArray() = readPlaybackSpeedArray(appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeedArray.name, null))
fun setPlaybackSpeedArray(speeds: List<Float>) {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
val speedFormat = DecimalFormat("0.00", format)
val jsonArray = JSONArray()
for (speed in speeds) jsonArray.put(speedFormat.format(speed.toDouble()))
appPrefs.edit().putString(UserPreferences.Prefs.prefPlaybackSpeedArray.name, jsonArray.toString()).apply()
}
fun setCurTempSpeed(speed: Float) {
curState = upsertBlk(curState) { it.curTempSpeed = speed }
}
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = onDismiss) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
dialogWindowProvider?.window?.let { window ->
window.setGravity(Gravity.BOTTOM)
window.setDimAmount(0f)
}
Card(modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 10.dp, bottom = 10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column {
var speed by remember { mutableStateOf(curSpeedFB) }
var speeds = remember { readPlaybackSpeedArray(appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeedArray.name, null)).toMutableStateList() }
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.playback_speed), fontSize = MaterialTheme.typography.headlineSmall.fontSize, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 4.dp))
Spacer(Modifier.width(50.dp))
FilterChip(onClick = {
if (speed !in speeds) {
speeds.add(speed)
speeds.sort()
setPlaybackSpeedArray(speeds)
} }, label = { Text(String.format(Locale.getDefault(), "%.2f", speed)) }, selected = false,
trailingIcon = { Icon(imageVector = Icons.Filled.Add, contentDescription = "Add icon", modifier = Modifier.size(FilterChipDefaults.IconSize)) })
}
var sliderPosition by remember { mutableFloatStateOf(speed2Slider(if (speed == FeedPreferences.SPEED_USE_GLOBAL) 1f else speed)) }
val stepSize = 0.05f
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Text("-", fontSize = MaterialTheme.typography.headlineLarge.fontSize, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable(onClick = {
val speed_ = round(speed / stepSize) * stepSize - stepSize
if (speed_ >= 0.1f) {
speed = speed_
sliderPosition = speed2Slider(speed)
}
}))
Slider(value = sliderPosition, modifier = Modifier.weight(1f).height(10.dp).padding(start = 20.dp, end = 20.dp),
onValueChange = {
sliderPosition = it
speed = slider2Speed(sliderPosition)
Logd("PlaybackSpeedDialog", "slider value: $it $speed}")
})
Text("+", fontSize = MaterialTheme.typography.headlineLarge.fontSize, fontWeight = FontWeight.Bold,
modifier = Modifier.clickable(onClick = {
val speed_ = round(speed / stepSize) * stepSize + stepSize
if (speed_ <= maxSpeed) {
speed = speed_
sliderPosition = speed2Slider(speed)
}
}))
}
var forCurrent by remember { mutableStateOf(indexDefault == 0) }
var forPodcast by remember { mutableStateOf(indexDefault == 1) }
var forGlobal by remember { mutableStateOf(indexDefault == 2) }
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.weight(1f))
Checkbox(checked = forCurrent, onCheckedChange = { isChecked -> forCurrent = isChecked })
Text(stringResource(R.string.current_episode))
Spacer(Modifier.weight(1f))
Checkbox(checked = forPodcast, onCheckedChange = { isChecked -> forPodcast = isChecked })
Text(stringResource(R.string.current_podcast))
Spacer(Modifier.weight(1f))
Checkbox(checked = forGlobal, onCheckedChange = { isChecked -> forGlobal = isChecked })
Text(stringResource(R.string.global))
Spacer(Modifier.weight(1f))
}
FlowRow(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), horizontalArrangement = Arrangement.spacedBy(15.dp)) {
speeds.forEach { chipSpeed ->
FilterChip(onClick = {
Logd("VariableSpeedDialog", "holder.chip settingCode0: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
settingCode[0] = forCurrent
settingCode[1] = forPodcast
settingCode[2] = forGlobal
Logd("VariableSpeedDialog", "holder.chip settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (playbackService != null) {
playbackService!!.isSpeedForward = false
playbackService!!.isFallbackSpeed = false
if (settingCode.size == 3) {
Logd(TAG, "setSpeed codeArray: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (settingCode[2]) UserPreferences.setPlaybackSpeed(chipSpeed)
if (settingCode[1]) {
val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode
if (episode?.feed?.preferences != null) upsertBlk(episode.feed!!) { it.preferences!!.playSpeed = chipSpeed }
}
if (settingCode[0]) {
setCurTempSpeed(chipSpeed)
playbackService!!.mPlayer?.setPlaybackParams(chipSpeed, isSkipSilence)
}
} else {
setCurTempSpeed(chipSpeed)
playbackService!!.mPlayer?.setPlaybackParams(chipSpeed, isSkipSilence)
}
}
else {
UserPreferences.setPlaybackSpeed(chipSpeed)
EventFlow.postEvent(FlowEvent.SpeedChangedEvent(chipSpeed))
}
onDismiss()
}, label = { Text(String.format(Locale.getDefault(), "%.2f", chipSpeed)) }, selected = false,
trailingIcon = { Icon(imageVector = Icons.Filled.Close, contentDescription = "Close icon",
modifier = Modifier.size(30.dp).padding(start = 10.dp).clickable(onClick = {
speeds.remove(chipSpeed)
setPlaybackSpeedArray(speeds)
})) })
}
}
var isSkipSilence by remember { mutableStateOf(false) }
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = isSkipSilence, onCheckedChange = { isChecked ->
isSkipSilence = isChecked
playbackService?.mPlayer?.setPlaybackParams(playbackService!!.curSpeed, isChecked)
})
Text(stringResource(R.string.pref_skip_silence_title))
}
}
}

View File

@ -1,295 +0,0 @@
package ac.mdiq.podcini.ui.dialog
//import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration
import ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
open class VariableSpeedDialog : BottomSheetDialogFragment() {
private var _binding: SpeedSelectDialogBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: SpeedSelectionAdapter
private lateinit var speedSeekBar: PlaybackSpeedSeekBar
private lateinit var addCurrentSpeedChip: Chip
private lateinit var settingCode: BooleanArray
private val selectedSpeeds: MutableList<Float>
init {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
selectedSpeeds = ArrayList(playbackSpeedArray)
}
override fun onStart() {
super.onStart()
Logd(TAG, "onStart: playbackService ready: ${playbackService?.isServiceReady()}")
binding.currentAudio.visibility = View.VISIBLE
binding.currentPodcast.visibility = View.VISIBLE
if (!settingCode[0]) binding.currentAudio.visibility = View.INVISIBLE
if (!settingCode[1]) binding.currentPodcast.visibility = View.INVISIBLE
if (!settingCode[2]) binding.global.visibility = View.INVISIBLE
procFlowEvents()
updateSpeed(FlowEvent.SpeedChangedEvent(curSpeedFB))
}
override fun onStop() {
super.onStop()
cancelFlowEvents()
}
private var eventSink: Job? = null
private fun cancelFlowEvents() {
eventSink?.cancel()
eventSink = null
}
private fun procFlowEvents() {
if (eventSink != null) return
eventSink = lifecycleScope.launch {
EventFlow.events.collectLatest { event ->
when (event) {
is FlowEvent.SpeedChangedEvent -> updateSpeed(event)
else -> {}
}
}
}
}
private fun updateSpeed(event: FlowEvent.SpeedChangedEvent) {
speedSeekBar.updateSpeed(event.newSpeed)
addCurrentSpeedChip.text = String.format(Locale.getDefault(), "%1$.2f", event.newSpeed)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = SpeedSelectDialogBinding.inflate(inflater)
settingCode = (arguments?.getBooleanArray("settingCode") ?: BooleanArray(3) {true})
val indexDefault = arguments?.getInt(INDEX_DEFAULT)
when (indexDefault) {
null, 0 -> binding.currentAudio.isChecked = true
1 -> binding.currentPodcast.isChecked = true
else -> binding.global.isChecked = true
}
speedSeekBar = binding.speedSeekBar
speedSeekBar.setProgressChangedListener { multiplier: Float ->
addCurrentSpeedChip.text = String.format(Locale.getDefault(), "%1$.2f", multiplier)
// controller?.setPlaybackSpeed(multiplier)
}
val selectedSpeedsGrid = binding.selectedSpeedsGrid
selectedSpeedsGrid.layoutManager = GridLayoutManager(context, 3)
selectedSpeedsGrid.addItemDecoration(ItemOffsetDecoration(requireContext(), 4))
adapter = SpeedSelectionAdapter()
adapter.setHasStableIds(true)
selectedSpeedsGrid.adapter = adapter
addCurrentSpeedChip = binding.addCurrentSpeedChip
addCurrentSpeedChip.isCloseIconVisible = true
addCurrentSpeedChip.setCloseIconResource(R.drawable.ic_add)
addCurrentSpeedChip.setOnCloseIconClickListener { addCurrentSpeed() }
addCurrentSpeedChip.closeIconContentDescription = getString(R.string.add_preset)
addCurrentSpeedChip.setOnClickListener { addCurrentSpeed() }
val skipSilence = binding.skipSilence
skipSilence.isChecked = isSkipSilence
skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
isSkipSilence = isChecked
// setSkipSilence(isChecked)
playbackService?.mPlayer?.setPlaybackParams(playbackService!!.curSpeed, isChecked)
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (playbackService?.isServiceReady() == false) {
binding.currentAudio.visibility = View.INVISIBLE
binding.currentPodcast.visibility = View.INVISIBLE
} else {
binding.currentAudio.visibility = View.VISIBLE
binding.currentPodcast.visibility = View.VISIBLE
}
}
override fun onDestroyView() {
Logd(TAG, "onDestroyView")
_binding = null
super.onDestroyView()
}
private fun addCurrentSpeed() {
val newSpeed = speedSeekBar.currentSpeed
if (selectedSpeeds.contains(newSpeed)) Snackbar.make(addCurrentSpeedChip, getString(R.string.preset_already_exists, newSpeed), Snackbar.LENGTH_LONG).show()
else {
selectedSpeeds.add(newSpeed)
selectedSpeeds.sort()
playbackSpeedArray = selectedSpeeds
adapter.notifyDataSetChanged()
}
}
var playbackSpeedArray: List<Float>
get() = readPlaybackSpeedArray(appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeedArray.name, null))
set(speeds) {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
val speedFormat = DecimalFormat("0.00", format)
val jsonArray = JSONArray()
for (speed in speeds) {
jsonArray.put(speedFormat.format(speed.toDouble()))
}
appPrefs.edit().putString(UserPreferences.Prefs.prefPlaybackSpeedArray.name, jsonArray.toString()).apply()
}
private fun readPlaybackSpeedArray(valueFromPrefs: String?): List<Float> {
if (valueFromPrefs != null) {
try {
val jsonArray = JSONArray(valueFromPrefs)
val selectedSpeeds: MutableList<Float> = ArrayList()
for (i in 0 until jsonArray.length()) {
selectedSpeeds.add(jsonArray.getDouble(i).toFloat())
}
return selectedSpeeds
} catch (e: JSONException) {
Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray")
e.printStackTrace()
}
}
// If this preference hasn't been set yet, return the default options
return mutableListOf(1.0f, 1.25f, 1.5f)
}
inner class SpeedSelectionAdapter : RecyclerView.Adapter<SpeedSelectionAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val chip = Chip(context)
chip.textAlignment = View.TEXT_ALIGNMENT_CENTER
return ViewHolder(chip)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val speed = selectedSpeeds[position]
holder.chip.text = String.format(Locale.getDefault(), "%1$.2f", speed)
holder.chip.setOnLongClickListener {
selectedSpeeds.remove(speed)
playbackSpeedArray = selectedSpeeds
notifyDataSetChanged()
true
}
holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({
Logd("VariableSpeedDialog", "holder.chip settingCode0: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
settingCode[0] = binding.currentAudio.isChecked
settingCode[1] = binding.currentPodcast.isChecked
settingCode[2] = binding.global.isChecked
Logd("VariableSpeedDialog", "holder.chip settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
dismiss()
setPlaybackSpeed(speed, settingCode)
}, 200) }
}
override fun getItemCount(): Int {
return selectedSpeeds.size
}
override fun getItemId(position: Int): Long {
return selectedSpeeds[position].hashCode().toLong()
}
private fun setPlaybackSpeed(speed: Float, codeArray: BooleanArray? = null) {
if (playbackService != null) {
playbackService!!.isSpeedForward = false
playbackService!!.isFallbackSpeed = false
if (codeArray != null && codeArray.size == 3) {
Logd(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
if (codeArray[2]) UserPreferences.setPlaybackSpeed(speed)
if (codeArray[1]) {
val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode
if (episode?.feed?.preferences != null) upsertBlk(episode.feed!!) { it.preferences!!.playSpeed = speed }
}
if (codeArray[0]) {
setCurTempSpeed(speed)
playbackService!!.mPlayer?.setPlaybackParams(speed, isSkipSilence)
}
} else {
setCurTempSpeed(speed)
playbackService!!.mPlayer?.setPlaybackParams(speed, isSkipSilence)
}
}
else {
UserPreferences.setPlaybackSpeed(speed)
EventFlow.postEvent(FlowEvent.SpeedChangedEvent(speed))
}
}
private fun setCurTempSpeed(speed: Float) {
curState = upsertBlk(curState) { it.curTempSpeed = speed }
}
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
}
companion object {
private val TAG: String = VariableSpeedDialog::class.simpleName ?: "Anonymous"
private const val INDEX_DEFAULT = "index_default"
/**
* @param settingCode_ array at input indicate which categories can be set, at output which categories are changed
* @param indexDefault indicates which category is checked by default
*/
fun newInstance(settingCode_: BooleanArray? = null, indexDefault: Int? = null): VariableSpeedDialog? {
val settingCode = settingCode_ ?: BooleanArray(3){true}
Logd("VariableSpeedDialog", "newInstance settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (settingCode.size != 3) {
Log.e("VariableSpeedDialog", "wrong settingCode dimension")
return null
}
val dialog = VariableSpeedDialog()
val args = Bundle()
args.putBooleanArray("settingCode", settingCode)
if (indexDefault != null) args.putInt(INDEX_DEFAULT, indexDefault)
dialog.arguments = args
return dialog
}
}
}

View File

@ -39,6 +39,7 @@ import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.compose.ChaptersDialog
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog
import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
@ -63,7 +64,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
@ -94,7 +100,9 @@ import net.dankito.readability4j.Readability4J
import org.apache.commons.lang3.StringUtils
import java.text.DecimalFormat
import java.text.NumberFormat
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.sin
class AudioPlayerFragment : Fragment() {
val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences("AudioPlayerFragmentPrefs", Context.MODE_PRIVATE) }
@ -117,6 +125,7 @@ class AudioPlayerFragment : Fragment() {
private var imgLoc by mutableStateOf<String?>(null)
private var imgLocLarge by mutableStateOf<String?>(null)
private var txtvPlaybackSpeed by mutableStateOf("")
private var curPlaybackSpeed by mutableStateOf(1f)
private var remainingTime by mutableIntStateOf(0)
private var isVideoScreen = false
private var playButRes by mutableIntStateOf(R.drawable.ic_play_48dp)
@ -125,6 +134,7 @@ class AudioPlayerFragment : Fragment() {
private var txtvLengtTexth by mutableStateOf("")
private var sliderValue by mutableFloatStateOf(0f)
private var sleepTimerActive by mutableStateOf(isSleepTimerActive())
private var showSpeedDialog by mutableStateOf(false)
private var shownotesCleaner: ShownotesCleaner? = null
@ -156,6 +166,7 @@ class AudioPlayerFragment : Fragment() {
val composeView = ComposeView(requireContext()).apply {
setContent {
CustomTheme(requireContext()) {
if (showSpeedDialog) PlaybackSpeedFullDialog(settingCode = booleanArrayOf(true, true, true), indexDefault = 0, maxSpeed = 3f, onDismiss = {showSpeedDialog = false})
Box(modifier = Modifier.fillMaxWidth().then(if (isCollapsed) Modifier else Modifier.statusBarsPadding().navigationBarsPadding())) {
PlayerUI(Modifier.align(if (isCollapsed) Alignment.TopCenter else Alignment.BottomCenter).zIndex(1f))
if (!isCollapsed) {
@ -188,6 +199,22 @@ class AudioPlayerFragment : Fragment() {
fun ControlUI() {
val textColor = MaterialTheme.colorScheme.onSurface
val context = LocalContext.current
@Composable
fun SpeedometerWithArc(speed: Float, maxSpeed: Float, trackColor: Color, modifier: Modifier) {
val needleAngle = (speed / maxSpeed) * 270f - 225
Canvas(modifier = modifier) {
val radius = 1.3 * size.minDimension / 2
val strokeWidth = 6.dp.toPx()
val arcRect = Rect(left = strokeWidth / 2, top = strokeWidth / 2, right = size.width - strokeWidth / 2, bottom = size.height - strokeWidth / 2)
drawArc(color = trackColor, startAngle = 135f, sweepAngle = 270f, useCenter = false, style = Stroke(width = strokeWidth), topLeft = arcRect.topLeft, size = arcRect.size)
val needleAngleRad = Math.toRadians(needleAngle.toDouble())
val needleEnd = Offset(x = size.center.x + (radius * 0.7f * cos(needleAngleRad)).toFloat(), y = size.center.y + (radius * 0.7f * sin(needleAngleRad)).toFloat())
drawLine(color = Color.Red, start = size.center, end = needleEnd, strokeWidth = 4.dp.toPx(), cap = StrokeCap.Round)
drawCircle(color = Color.Cyan, center = size.center, radius = 3.dp.toPx())
}
}
Row {
fun ensureService() {
if (curMedia == null) return
@ -219,10 +246,15 @@ class AudioPlayerFragment : Fragment() {
}))
Spacer(Modifier.weight(0.1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playback_speed), tint = textColor, contentDescription = "speed",
SpeedometerWithArc(speed = curPlaybackSpeed*100, maxSpeed = 300f, trackColor = textColor,
modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = {
VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
}))
showSpeedDialog = true
// VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
}))
// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playback_speed), tint = textColor, contentDescription = "speed",
// modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = {
// VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
// }))
Text(txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodySmall)
}
Spacer(Modifier.weight(0.1f))
@ -555,6 +587,7 @@ class AudioPlayerFragment : Fragment() {
}
private fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
curPlaybackSpeed = event.newSpeed
txtvPlaybackSpeed = speedStr
}
@ -593,6 +626,7 @@ class AudioPlayerFragment : Fragment() {
Logd(TAG, "updateUi called $media")
titleText = media.getEpisodeTitle()
txtvPlaybackSpeed = DecimalFormat("0.00").format(curSpeedFB.toDouble())
curPlaybackSpeed = curSpeedFB
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) {

View File

@ -98,6 +98,7 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var showNewSynthetic by mutableStateOf(false)
var showSortDialog by mutableStateOf(false)
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
var layoutMode by mutableIntStateOf(0)
private val ioScope = CoroutineScope(Dispatchers.IO)
private var onInit: Boolean = true
@ -111,7 +112,6 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Logd(TAG, "fragment onCreateView")
_binding = ComposeFragmentBinding.inflate(inflater)
sortOrder = feed?.sortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
@ -164,6 +164,7 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
loadItemsRunning = false
}
}
layoutMode = if (feed?.preferences?.useWideLayout == true) 1 else 0
binding.mainView.setContent {
CustomTheme(requireContext()) {
if (showRemoveFeedDialog) RemoveFeedDialog(listOf(feed!!), onDismissRequest = {showRemoveFeedDialog = false}) {
@ -198,7 +199,7 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column {
FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = filterButtonColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()})
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
EpisodeLazyColumn(activity as MainActivity, vms = vms, feed = feed,
EpisodeLazyColumn(activity as MainActivity, vms = vms, feed = feed, layoutMode = layoutMode,
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
leftSwipeCB = {
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
@ -598,6 +599,7 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
feed = withContext(Dispatchers.IO) {
val feed_ = getFeed(feedID)
if (feed_ != null) {
layoutMode = if (feed_.preferences?.useWideLayout == true) 1 else 0
Logd(TAG, "loadItems feed_.episodes.size: ${feed_.episodes.size}")
val etmp = mutableListOf<Episode>()
if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) {

View File

@ -2,7 +2,6 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.FeedsettingsBinding
import ac.mdiq.podcini.databinding.PlaybackSpeedFeedSettingDialogBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.playback.base.VideoMode.Companion.videoModeTags
@ -17,10 +16,10 @@ import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDownloadPolicy
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.PlaybackSpeedDialog
import ac.mdiq.podcini.ui.compose.Spinner
import ac.mdiq.podcini.ui.compose.TagSettingDialog
import ac.mdiq.podcini.util.Logd
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@ -28,10 +27,8 @@ import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@ -58,8 +55,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.*
class FeedSettingsFragment : Fragment() {
private var _binding: FeedsettingsBinding? = null
@ -100,6 +95,22 @@ class FeedSettingsFragment : Fragment() {
CustomTheme(requireContext()) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.padding(start = 20.dp, end = 16.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.rounded_responsive_layout_24), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.use_wide_layout), style = MaterialTheme.typography.titleLarge, color = textColor)
Spacer(modifier = Modifier.weight(1f))
var checked by remember { mutableStateOf(feed?.preferences?.useWideLayout == true) }
Switch(checked = checked, modifier = Modifier.height(24.dp),
onCheckedChange = {
checked = it
feed = upsertBlk(feed!!) { f -> f.preferences?.useWideLayout = checked }
}
)
}
Text(text = stringResource(R.string.use_wide_layout_summary), style = MaterialTheme.typography.bodyMedium, color = textColor)
}
if ((feed?.id ?: 0) > MAX_SYNTHETIC_ID) {
// refresh
Column {
@ -108,7 +119,7 @@ class FeedSettingsFragment : Fragment() {
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.keep_updated), style = MaterialTheme.typography.titleLarge, color = textColor)
Spacer(modifier = Modifier.weight(1f))
var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated != false) }
var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated == true) }
Switch(checked = checked, modifier = Modifier.height(24.dp),
onCheckedChange = {
checked = it
@ -273,10 +284,15 @@ class FeedSettingsFragment : Fragment() {
// playback speed
Column {
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) PlaybackSpeedDialog(listOf(feed!!), initSpeed = feed!!.preferences!!.playSpeed, maxSpeed = 3f,
onDismiss = { showDialog.value = false }) { newSpeed ->
feed = upsertBlk(feed!!) { it.preferences?.playSpeed = newSpeed }
}
Icon(ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.playback_speed), style = MaterialTheme.typography.titleLarge, color = textColor,
modifier = Modifier.clickable(onClick = { PlaybackSpeedDialog().show() }))
modifier = Modifier.clickable(onClick = { showDialog.value = true }))
}
Text(text = stringResource(R.string.pref_feed_playback_speed_sum), style = MaterialTheme.typography.bodyMedium, color = textColor)
}
@ -770,29 +786,6 @@ class FeedSettingsFragment : Fragment() {
}
}
private fun PlaybackSpeedDialog(): AlertDialog {
val binding = PlaybackSpeedFeedSettingDialogBinding.inflate(LayoutInflater.from(requireContext()))
binding.seekBar.setProgressChangedListener { speed: Float? ->
binding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed)
}
binding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
binding.seekBar.isEnabled = !isChecked
binding.seekBar.alpha = if (isChecked) 0.4f else 1f
binding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f
}
val speed = feed?.preferences!!.playSpeed
binding.useGlobalCheckbox.isChecked = speed == FeedPreferences.SPEED_USE_GLOBAL
binding.seekBar.updateSpeed(if (speed == FeedPreferences.SPEED_USE_GLOBAL) 1f else speed)
return MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.playback_speed).setView(binding.root)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val newSpeed = if (binding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
else binding.seekBar.currentSpeed
feed = upsertBlk(feed!!) { it.preferences?.playSpeed = newSpeed }
}
.setNegativeButton(R.string.cancel_label, null)
.create()
}
// class PositiveIntegerTransform : VisualTransformation {
// override fun filter(text: AnnotatedString): TransformedText {
// val trimmedText = text.text.filter { it.isDigit() }

View File

@ -40,8 +40,10 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -139,7 +141,8 @@ class OnlineSearchFragment : Fragment() {
@Composable
fun MainView() {
val textColor = MaterialTheme.colorScheme.onSurface
Column(Modifier.padding(horizontal = 10.dp)) {
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().padding(horizontal = 10.dp).verticalScroll(scrollState)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
var queryText by remember { mutableStateOf("") }
fun performSearch() {
@ -154,20 +157,20 @@ class OnlineSearchFragment : Fragment() {
}
QuickDiscoveryView()
Text(stringResource(R.string.advanced), color = textColor, fontWeight = FontWeight.Bold)
Text(stringResource(R.string.add_podcast_by_url), color = textColor, modifier = Modifier.clickable(onClick = { showAddViaUrlDialog() }))
Text(stringResource(R.string.add_local_folder), color = textColor, modifier = Modifier.clickable(onClick = {
Text(stringResource(R.string.add_podcast_by_url), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { showAddViaUrlDialog() }))
Text(stringResource(R.string.add_local_folder), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 4.dp).clickable(onClick = {
try { addLocalFolderLauncher.launch(null)
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
}
}))
Text(stringResource(R.string.search_vistaguide_label), color = textColor, modifier = Modifier.clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }))
Text(stringResource(R.string.search_itunes_label), color = textColor, modifier = Modifier.clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }))
Text(stringResource(R.string.search_fyyd_label), color = textColor, modifier = Modifier.clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }))
Text(stringResource(R.string.gpodnet_search_hint), color = textColor, modifier = Modifier.clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }))
Text(stringResource(R.string.search_podcastindex_label), color = textColor, modifier = Modifier.clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }))
Text(stringResource(R.string.opml_add_podcast_label), color = textColor, modifier = Modifier.clickable(onClick = {
Text(stringResource(R.string.search_vistaguide_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }))
Text(stringResource(R.string.search_itunes_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }))
Text(stringResource(R.string.search_fyyd_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }))
Text(stringResource(R.string.gpodnet_search_hint), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }))
Text(stringResource(R.string.search_podcastindex_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }))
Text(stringResource(R.string.opml_add_podcast_label), color = textColor, modifier = Modifier.padding(start = 10.dp, top = 5.dp).clickable(onClick = {
try { chooseOpmlImportPathLauncher.launch("*/*")
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
@ -197,9 +200,8 @@ class OnlineSearchFragment : Fragment() {
Spacer(Modifier.weight(1f))
Text(stringResource(R.string.discover_more), color = textColor, modifier = Modifier.clickable(onClick = {(activity as MainActivity).loadChildFragment(DiscoveryFragment())}))
}
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (grid, error) = createRefs()
if (showGrid) NonlazyGrid(columns = numColumns, itemCount = searchResult.size, modifier = Modifier.fillMaxWidth().constrainAs(grid) { centerTo(parent) }) { index ->
Box(modifier = Modifier.fillMaxWidth()) {
if (showGrid) NonlazyGrid(columns = numColumns, itemCount = searchResult.size, modifier = Modifier.fillMaxWidth()) { index ->
AsyncImage(model = ImageRequest.Builder(context).data(searchResult[index].imageUrl)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
contentDescription = "imgvCover", modifier = Modifier.padding(top = 8.dp)
@ -212,7 +214,7 @@ class OnlineSearchFragment : Fragment() {
}
}))
}
if (showError) Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().constrainAs(error) { centerTo(parent) }) {
if (showError) Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
Text(errorText, color = textColor)
if (showRetry) Button(onClick = {
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()

View File

@ -3,7 +3,6 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.databinding.DialogSwitchPreferenceBinding
import ac.mdiq.podcini.databinding.PlaybackSpeedFeedSettingDialogBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.preferences.UserPreferences
@ -32,7 +31,6 @@ import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.CompoundButton
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
@ -528,6 +526,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (showKeepUpdateDialog) SetKeepUpdateDialog {showKeepUpdateDialog = false}
var showTagsSettingDialog by remember { mutableStateOf(false) }
if (showTagsSettingDialog) TagSettingDialog(selected) { showTagsSettingDialog = false }
var showSpeedDialog by remember { mutableStateOf(false) }
if (showSpeedDialog) PlaybackSpeedDialog(selected, initSpeed = 1f, maxSpeed = 3f, onDismiss = {showSpeedDialog = false}) { newSpeed ->
saveFeedPreferences { it: FeedPreferences -> it.playSpeed = newSpeed }
}
@Composable
fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList<Feed>, modifier: Modifier = Modifier) {
@ -576,28 +578,30 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
isExpanded = false
selectMode = false
Logd(TAG, "ic_playback_speed: ${selected.size}")
val vBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater)
vBinding.seekBar.setProgressChangedListener { speed: Float? ->
vBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed)
}
vBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
vBinding.seekBar.isEnabled = !isChecked
vBinding.seekBar.alpha = if (isChecked) 0.4f else 1f
vBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f
}
vBinding.seekBar.updateSpeed(1.0f)
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.playback_speed)
.setView(vBinding.root)
.setPositiveButton("OK") { _: DialogInterface?, _: Int ->
val newSpeed = if (vBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
else vBinding.seekBar.currentSpeed
saveFeedPreferences { it: FeedPreferences ->
it.playSpeed = newSpeed
}
}
.setNegativeButton(R.string.cancel_label, null)
.show()
showSpeedDialog = true
// val vBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater)
// vBinding.seekBar.setProgressChangedListener { speed: Float? ->
// vBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed)
// }
// vBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
// vBinding.seekBar.isEnabled = !isChecked
// vBinding.seekBar.alpha = if (isChecked) 0.4f else 1f
// vBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f
// }
// vBinding.seekBar.updateSpeed(1.0f)
// MaterialAlertDialogBuilder(activity)
// .setTitle(R.string.playback_speed)
// .setView(vBinding.root)
// .setPositiveButton("OK") { _: DialogInterface?, _: Int ->
// val newSpeed = if (vBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
// else vBinding.seekBar.currentSpeed
// saveFeedPreferences { it: FeedPreferences ->
// it.playSpeed = newSpeed
// }
// }
// .setNegativeButton(R.string.cancel_label, null)
// .show()
}) {
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "")
Text(stringResource(id = R.string.playback_speed)) } },

View File

@ -1,18 +0,0 @@
package ac.mdiq.podcini.ui.utils
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
/**
* Source: https://stackoverflow.com/a/30794046
*/
class ItemOffsetDecoration(context: Context, itemOffsetDp: Int) : RecyclerView.ItemDecoration() {
private val itemOffset = (itemOffsetDp * context.resources.displayMetrics.density).toInt()
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
outRect[itemOffset, itemOffset, itemOffset] = itemOffset
}
}

View File

@ -1,98 +0,0 @@
package ac.mdiq.podcini.ui.view
import ac.mdiq.podcini.R
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import kotlin.math.abs
import kotlin.math.min
class CircularProgressBar : View {
private val paintBackground = Paint()
private val paintProgress = Paint()
private var percentage = 0f
private var targetPercentage = 0f
private var isIndeterminate = false
private var tag: Any? = null
private val bounds = RectF()
constructor(context: Context) : super(context) {
setup(null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
setup(attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setup(attrs)
}
private fun setup(attrs: AttributeSet?) {
paintBackground.isAntiAlias = true
paintBackground.style = Paint.Style.STROKE
paintProgress.isAntiAlias = true
paintProgress.style = Paint.Style.STROKE
paintProgress.strokeCap = Paint.Cap.ROUND
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircularProgressBar)
val color = typedArray.getColor(R.styleable.CircularProgressBar_foregroundColor, Color.GREEN)
typedArray.recycle()
paintProgress.color = color
paintBackground.color = color
}
/**
* Sets the percentage to be displayed.
* @param percentage Number from 0 to 1
* @param tag When the tag is the same as last time calling setPercentage, the update is animated
*/
fun setPercentage(percentage: Float, tag: Any?) {
targetPercentage = percentage
if (tag == null || tag != this.tag) {
// Do not animate
this.percentage = percentage
this.tag = tag
}
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val padding = height * 0.07f
paintBackground.strokeWidth = height * 0.02f
paintBackground.setPathEffect(if (isIndeterminate) DASHED else null)
paintProgress.strokeWidth = padding
bounds[padding, padding, width - padding] = height - padding
canvas.drawArc(bounds, -90f, 360f, false, paintBackground)
if (percentage in MINIMUM_PERCENTAGE..MAXIMUM_PERCENTAGE)
canvas.drawArc(bounds, -90f, percentage * 360, false, paintProgress)
if (abs((percentage - targetPercentage).toDouble()) > MINIMUM_PERCENTAGE) {
var speed = 0.02f
if (abs((targetPercentage - percentage).toDouble()) < 0.1 && targetPercentage > percentage) speed = 0.006f
val delta = min(speed.toDouble(), abs((targetPercentage - percentage).toDouble())).toFloat()
val direction = (if ((targetPercentage - percentage) > 0) 1f else -1f)
percentage += delta * direction
invalidate()
}
}
fun setIndeterminate(indeterminate: Boolean) {
isIndeterminate = indeterminate
}
companion object {
const val MINIMUM_PERCENTAGE: Float = 0.005f
const val MAXIMUM_PERCENTAGE: Float = 1 - MINIMUM_PERCENTAGE
private val DASHED: PathEffect = DashPathEffect(floatArrayOf(5f, 5f), 0f)
}
}

View File

@ -1,106 +0,0 @@
package ac.mdiq.podcini.ui.view
import ac.mdiq.podcini.R
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import kotlin.math.*
class PlaybackSpeedIndicatorView : View {
private val arcPaint = Paint()
private val indicatorPaint = Paint()
private val trianglePath = Path()
private val arcBounds = RectF()
private var angle = VALUE_UNSET
private var targetAngle = VALUE_UNSET
private var degreePerFrame = 1.6f
private var paddingArc = 20f
private var paddingIndicator = 10f
constructor(context: Context) : super(context) {
setup(null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
setup(attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setup(attrs)
}
private fun setup(attrs: AttributeSet?) {
setSpeed(1.0f) // Set default angle to 1.0
targetAngle = VALUE_UNSET // Do not move to first value that is set externally
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PlaybackSpeedIndicatorView)
val color = typedArray.getColor(R.styleable.PlaybackSpeedIndicatorView_foregroundColor, Color.GREEN)
typedArray.recycle()
arcPaint.color = color
indicatorPaint.color = color
arcPaint.isAntiAlias = true
arcPaint.style = Paint.Style.STROKE
arcPaint.strokeCap = Paint.Cap.ROUND
indicatorPaint.isAntiAlias = true
indicatorPaint.style = Paint.Style.FILL
trianglePath.fillType = Path.FillType.EVEN_ODD
}
fun setSpeed(value: Float) {
val maxAnglePerDirection = 90 + 45 - 2 * paddingArc
// Speed values above 3 are probably not too common. Cap at 3 for better differentiation
val normalizedValue = (min(2.5, (value - 0.5f).toDouble()) / 2.5f).toFloat() // Linear between 0 and 1
val target = -maxAnglePerDirection + 2 * maxAnglePerDirection * normalizedValue
if (targetAngle == VALUE_UNSET) angle = target
targetAngle = target
degreePerFrame = (abs((targetAngle - angle).toDouble()) / 20).toFloat()
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
paddingArc = measuredHeight / 4.5f
paddingIndicator = measuredHeight / 6f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val radiusInnerCircle = width / 10f
canvas.drawCircle(width / 2f, height / 2f, radiusInnerCircle, indicatorPaint)
trianglePath.rewind()
val bigRadius = height / 2f - paddingIndicator
trianglePath.moveTo(width / 2f + (bigRadius * sin(((-angle + 180) * DEG_2_RAD).toDouble())).toFloat(),
height / 2f + (bigRadius * cos(((-angle + 180) * DEG_2_RAD).toDouble())).toFloat())
trianglePath.lineTo(width / 2f + (radiusInnerCircle * sin(((-angle + 180 - 90) * DEG_2_RAD).toDouble())).toFloat(),
height / 2f + (radiusInnerCircle * cos(((-angle + 180 - 90) * DEG_2_RAD).toDouble())).toFloat())
trianglePath.lineTo(width / 2f + (radiusInnerCircle * sin(((-angle + 180 + 90) * DEG_2_RAD).toDouble())).toFloat(),
height / 2f + (radiusInnerCircle * cos(((-angle + 180 + 90) * DEG_2_RAD).toDouble())).toFloat())
trianglePath.close()
canvas.drawPath(trianglePath, indicatorPaint)
arcPaint.strokeWidth = height / 15f
arcBounds[paddingArc, paddingArc, width - paddingArc] = height - paddingArc
canvas.drawArc(arcBounds, (-180 - 45).toFloat(), 90 + 45 + angle - PADDING_ANGLE, false, arcPaint)
canvas.drawArc(arcBounds, -90 + PADDING_ANGLE + angle, 90 + 45 - PADDING_ANGLE - angle, false, arcPaint)
if (abs((angle - targetAngle).toDouble()) > 0.5 && targetAngle != VALUE_UNSET) {
angle = (angle + sign((targetAngle - angle).toDouble()) * min(degreePerFrame.toDouble(), abs((targetAngle - angle).toDouble()))).toFloat()
invalidate()
}
}
companion object {
private const val DEG_2_RAD = (Math.PI / 180).toFloat()
private const val PADDING_ANGLE = 30f
private const val VALUE_UNSET = -4242f
}
}

View File

@ -1,65 +0,0 @@
package ac.mdiq.podcini.ui.view
import ac.mdiq.podcini.databinding.PlaybackSpeedSeekBarBinding
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.core.util.Consumer
import kotlin.math.roundToInt
class PlaybackSpeedSeekBar : FrameLayout {
private var _binding: PlaybackSpeedSeekBarBinding? = null
private val binding get() = _binding!!
private lateinit var seekBar: SeekBar
private var progressChangedListener: Consumer<Float>? = null
val currentSpeed: Float
get() = (seekBar.progress + 10) / 20.0f
constructor(context: Context) : super(context) {
setup()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
setup()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
setup()
}
private fun setup() {
_binding = PlaybackSpeedSeekBarBinding.inflate(LayoutInflater.from(context), this, true)
seekBar = binding.playbackSpeed
binding.butDecSpeed.setOnClickListener { seekBar.progress -= 2 }
binding.butIncSpeed.setOnClickListener { seekBar.progress += 2 }
seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
val playbackSpeed = (progress + 10) / 20.0f
progressChangedListener?.accept(playbackSpeed)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
}
fun updateSpeed(speedMultiplier: Float) {
seekBar.progress = ((20 * speedMultiplier) - 10).roundToInt()
}
fun setProgressChangedListener(progressChangedListener: Consumer<Float>?) {
this.progressChangedListener = progressChangedListener
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
seekBar.isEnabled = enabled
binding.butDecSpeed.isEnabled = enabled
binding.butIncSpeed.isEnabled = enabled
}
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M120,760L120,400Q120,367 143.5,343.5Q167,320 200,320L320,320L320,200Q320,167 343.5,143.5Q367,120 400,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840Q167,840 143.5,816.5Q120,793 120,760ZM640,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L400,200Q400,200 400,200Q400,200 400,200L400,320Q400,320 400,320Q400,320 400,320L560,320Q593,320 616.5,343.5Q640,367 640,400L640,760Q640,760 640,760Q640,760 640,760ZM400,760L560,760Q560,760 560,760Q560,760 560,760L560,400Q560,400 560,400Q560,400 560,400L400,400Q400,400 400,400Q400,400 400,400L400,760Q400,760 400,760Q400,760 400,760ZM200,760L320,760Q320,760 320,760Q320,760 320,760L320,400Q320,400 320,400Q320,400 320,400L200,400Q200,400 200,400Q200,400 200,400L200,760Q200,760 200,760Q200,760 200,760Z"/>
</vector>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<CheckBox
android:id="@+id/useGlobalCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/global_default"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/currentSpeedLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" />
</LinearLayout>
</LinearLayout>

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:id="@+id/playback_speed_seek_bar">
<TextView
android:id="@+id/butDecSpeed"
android:layout_width="40dp"
android:layout_height="40dp"
android:gravity="center"
android:text="-"
android:clickable="true"
android:focusable="true"
android:scrollbars="none"
android:textStyle="bold"
android:textSize="24sp"
android:textColor="?attr/colorSecondary"
android:contentDescription="@string/decrease_speed"
android:background="?attr/selectableItemBackgroundBorderless" />
<SeekBar
android:id="@+id/playback_speed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:max="70"
android:paddingVertical="4dp"
android:layout_weight="1" />
<TextView
android:id="@+id/butIncSpeed"
android:layout_width="40dp"
android:layout_height="40dp"
android:gravity="center"
android:text="+"
android:clickable="true"
android:focusable="true"
android:scrollbars="none"
android:textStyle="bold"
android:textSize="24sp"
android:textColor="?attr/colorSecondary"
android:contentDescription="@string/increase_speed"
android:background="?attr/selectableItemBackgroundBorderless" />
</LinearLayout>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:id="@+id/secondary_action"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="false"
android:focusableInTouchMode="false" >
<ImageView
android:id="@+id/secondaryActionIcon"
android:layout_width="18dp"
android:layout_height="21dp"
android:layout_gravity="center"
tools:ignore="ContentDescription"
tools:src="@sample/secondaryaction"/>
<ac.mdiq.podcini.ui.view.CircularProgressBar
android:id="@+id/secondaryActionProgress"
android:layout_width="40dp"
android:layout_gravity="center"
android:layout_height="40dp"
app:foregroundColor="?attr/action_icon_color"/>
</FrameLayout>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:padding="10dp"
android:ems="10" />

View File

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/speed_select_dialog"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/playback_speed"
style="@style/Podcini.TextView.ListItemPrimaryTitle" />
<com.google.android.material.chip.Chip
android:id="@+id/add_current_speed_chip"
android:gravity="center_vertical|start"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar
android:id="@+id/speed_seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/propertyOf"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<RadioButton
android:id="@+id/for_episode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Episode"
android:checked="true"/>
<RadioButton
android:id="@+id/for_podcast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/Podcast" />
<RadioButton
android:id="@+id/for_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/All" />
</RadioGroup>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/currentAudio"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:checked="true"
android:text="@string/current_episode" />
<CheckBox
android:id="@+id/currentPodcast"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/current_podcast" />
<CheckBox
android:id="@+id/global"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/global" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/selected_speeds_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/skipSilence"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_skip_silence_title" />
</LinearLayout>

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/edit_tags"
android:menuCategory="container"
android:title="@string/edit_tags" />
<item
android:id="@+id/rename_item"
android:menuCategory="container"
android:title="@string/rename_feed_label" />
<item
android:id="@+id/remove_feed"
android:menuCategory="container"
android:title="@string/remove_feed_label" />
<item
android:id="@+id/multi_select"
android:menuCategory="container"
android:title="@string/multi_select"
android:visible="false" />
</menu>

View File

@ -839,6 +839,8 @@
<string name="include_terms">Include only episodes containing any of the terms below</string>
<string name="exclude_episodes_shorter_than">Exclude episodes shorter than</string>
<string name="mark_excluded_episodes_played">Mark excluded episodes played</string>
<string name="use_wide_layout">Use wide layout</string>
<string name="use_wide_layout_summary">Wide layout on episode list shows larger image, more suitable for video contents.</string>
<string name="keep_updated">Keep updated</string>
<string name="keep_updated_summary">Include this podcast when (auto-)refreshing all podcasts</string>
<string name="audo_add_new_queue">Auto add new to queue</string>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircularProgressBar">
<attr name="foregroundColor" format="color" />
</declare-styleable>
<!-- <declare-styleable name="CircularProgressBar">-->
<!-- <attr name="foregroundColor" format="color" />-->
<!-- </declare-styleable>-->
<declare-styleable name="PlaybackSpeedIndicatorView">
<attr name="foregroundColor" /> <!-- format omitted to avoid double definition -->
</declare-styleable>
<!-- <declare-styleable name="PlaybackSpeedIndicatorView">-->
<!-- <attr name="foregroundColor" /> &lt;!&ndash; format omitted to avoid double definition &ndash;&gt;-->
<!-- </declare-styleable>-->
</resources>

View File

@ -1,3 +1,17 @@
# 6.14.4
* a new speedometer on the player UI
* adjusted padding in advanced options in OnlineSearch view
* in episode lists, added view count (available in Youtube contents)
* amended episode lists layout
* playState marking on the image is removed
* title is at the top and takes full length to the right of the image
* the action button (play/pause etc) is at the lower right corner may overlay on other text if any
* colored some actionable icons
* created a new layout for FeedEpsiodes with a larger image, more suitable for video contents
* in feed settings created useWideLayout for choosing the desired layout
* reworked speed setting dialogs in Compose and removed unused old codes
# 6.14.3
* fixed crash when constructing TTS

View File

@ -0,0 +1,13 @@
Version 6.14.4
* a new speedometer on the player UI
* adjusted padding in advanced options in OnlineSearch view
* in episode lists, added view count (available in Youtube contents)
* amended episode lists layout
* playState marking on the image is removed
* title is at the top and takes full length to the right of the image
* the action button (play/pause etc) is at the lower right corner may overlay on other text if any
* colored some actionable icons
* created a new layout for FeedEpsiodes with a larger image, more suitable for video contents
* in feed settings created useWideLayout for choosing the desired layout
* reworked speed setting dialogs in Compose and removed unused old codes