6.14.4 commit
This commit is contained in:
parent
d22f29d4be
commit
ddc0b09a38
|
@ -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
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)) } },
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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" /> <!– format omitted to avoid double definition –>-->
|
||||
<!-- </declare-styleable>-->
|
||||
</resources>
|
||||
|
|
14
changelog.md
14
changelog.md
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue