7.3.1 commit

This commit is contained in:
Xilin Jia 2025-01-08 23:17:04 +01:00
parent 6e26b80488
commit d913f2474d
23 changed files with 303 additions and 703 deletions

View File

@ -38,8 +38,8 @@ This project was developed from a fork of [AntennaPod](<https://github.com/Anten
7. Promotes auto-download governed by policy and limit settings of individual feed (podcast).
8. Supports casting Youtube audio/video contents to a Chromecast speaker/screen (the play app)
9. Spotlights `instant sync` across devices without a server.
10. Offers Readability and Text-to-Speech for RSS contents,
11. Replaced SQLite with modern object-base Realm DB, Glide with Coil, RxJava and threads with coroutines, EventBus with SharedFlow and fragments with screens,
10. Offers Readability and Text-to-Speech for RSS contents.
11. Replaced SQLite with modern object-base Realm DB, Glide with Coil, RxJava and threads with coroutines, EventBus with SharedFlow, and fragments with screens.
The project aims to profit from modern frameworks, improve efficiency and provide more useful and user-friendly features.

View File

@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []
versionCode 3020337
versionName "7.3.0"
versionCode 3020338
versionName "7.3.1"
ndkVersion "27.0.12077973"
@ -194,7 +194,7 @@ dependencies {
implementation "androidx.webkit:webkit:1.12.1"
implementation "androidx.window:window:1.3.0"
implementation "androidx.work:work-runtime:2.10.0"
implementation "androidx.fragment:fragment-ktx:1.8.5"
// implementation "androidx.fragment:fragment-ktx:1.8.5"
implementation "androidx.media3:media3-exoplayer:1.5.1"
implementation "androidx.media3:media3-datasource-okhttp:1.5.1"

View File

@ -7,6 +7,7 @@ import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -27,7 +28,7 @@ import kotlinx.coroutines.launch
* Communicates with the playback service. GUI classes should use this class to
* control playback instead of communicating with the PlaybackService directly.
*/
abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
abstract class ServiceStatusHandler(private val activity: MainActivity) {
private var mediaInfoLoaded = false
private var loadedFeedMediaId: Long = -1

View File

@ -14,6 +14,7 @@ import android.content.ClipboardManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
@ -38,7 +39,7 @@ import java.io.FileInputStream
import java.io.IOException
import java.nio.charset.Charset
class BugReportActivity : AppCompatActivity() {
class BugReportActivity : ComponentActivity() {
private var crashDetailsTextView by mutableStateOf("")
var showToast by mutableStateOf(false)
var toastMassege by mutableStateOf("")

View File

@ -26,6 +26,7 @@ import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.*
@ -66,7 +67,7 @@ import java.io.IOException
import java.io.InputStreamReader
import javax.xml.parsers.DocumentBuilderFactory
class PreferenceActivity : AppCompatActivity() {
class PreferenceActivity : ComponentActivity() {
var copyrightNoticeText by mutableStateOf("")
var showToast by mutableStateOf(false)
var toastMassege by mutableStateOf("")
@ -140,15 +141,15 @@ class PreferenceActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
if (supportFragmentManager.backStackEntryCount == 0) finish()
else {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
var view = currentFocus
//If no view currently has focus, create a new one, just so we can grab a window token from it
if (view == null) view = View(this)
imm.hideSoftInputFromWindow(view.windowToken, 0)
supportFragmentManager.popBackStack()
}
// if (supportFragmentManager.backStackEntryCount == 0) finish()
// else {
// val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
// var view = currentFocus
// //If no view currently has focus, create a new one, just so we can grab a window token from it
// if (view == null) view = View(this)
// imm.hideSoftInputFromWindow(view.windowToken, 0)
// supportFragmentManager.popBackStack()
// }
return true
}
return false

View File

@ -14,15 +14,15 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.net.URL
import java.net.URLDecoder
class ShareReceiverActivity : AppCompatActivity() {
class ShareReceiverActivity : ComponentActivity() {
private var sharedUrl: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
@ -103,7 +103,7 @@ class ShareReceiverActivity : AppCompatActivity() {
const val ARG_FEEDURL: String = "arg.feedurl"
private const val RESULT_ERROR = 2
fun receiveShared(sharedUrl: String, activity: AppCompatActivity, finish: Boolean, mediaCB: ()->Unit) {
fun receiveShared(sharedUrl: String, activity: ComponentActivity, finish: Boolean, mediaCB: ()->Unit) {
val url = URL(sharedUrl)
val log = realm.query(ShareLog::class).query("url == $0", sharedUrl).first().find()
when {

View File

@ -10,6 +10,7 @@ import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.BorderStroke
@ -37,7 +38,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubscriptionShortcutActivity : AppCompatActivity() {
class SubscriptionShortcutActivity : ComponentActivity() {
private val listItems = mutableStateListOf<Feed>()
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -17,16 +17,15 @@ import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerAc
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme
import ac.mdiq.podcini.preferences.AppPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.AppPreferences.rewindSecs
import ac.mdiq.podcini.preferences.AppPreferences.videoPlayMode
import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.ui.utils.starter.MainActivityStarter
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.utils.starter.MainActivityStarter
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@ -99,6 +98,8 @@ class VideoplayerActivity : CastEnabledActivity() {
val showErrorDialog = mutableStateOf(false)
var errorMessage by mutableStateOf("")
var showSleepTimeDialog by mutableStateOf(false)
var showShareDialog by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
@ -141,6 +142,8 @@ class VideoplayerActivity : CastEnabledActivity() {
else showShareDialog = false
}
if (showSleepTimeDialog) SleepTimerDialog { showSleepTimeDialog = false }
LaunchedEffect(curMediaId) { cleanedNotes = null }
Scaffold(topBar = { MyTopAppBar() }) { innerPadding ->
@ -344,7 +347,8 @@ class VideoplayerActivity : CastEnabledActivity() {
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) supportInvalidateOptionsMenu()
// TODO
// is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) supportInvalidateOptionsMenu()
is FlowEvent.PlaybackServiceEvent -> if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) finish()
is FlowEvent.MessageEvent -> onEventMainThread(event)
is FlowEvent.PlayerErrorEvent -> {
@ -380,7 +384,8 @@ class VideoplayerActivity : CastEnabledActivity() {
actions = {
if (!landscape) {
var sleepIconRes by remember { mutableIntStateOf(if (!isSleepTimerActive()) R.drawable.ic_sleep else R.drawable.ic_sleep_off) }
IconButton(onClick = { SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
IconButton(onClick = { showSleepTimeDialog = true
// SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
}) { Icon(imageVector = ImageVector.vectorResource(sleepIconRes), contentDescription = "sleeper") }
IconButton(onClick = { showSpeedDialog = true
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playback_speed), contentDescription = "open podcast") }
@ -405,7 +410,8 @@ class VideoplayerActivity : CastEnabledActivity() {
if (landscape) {
var sleeperRes by remember { mutableIntStateOf(if (!isSleepTimerActive()) R.string.set_sleeptimer_label else R.string.sleep_timer_label) }
DropdownMenuItem(text = { Text(stringResource(sleeperRes)) }, onClick = {
SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
showSleepTimeDialog = true
// SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
expanded = false
})
DropdownMenuItem(text = { Text(stringResource(R.string.player_switch_to_audio_only)) }, onClick = {

View File

@ -1,13 +1,38 @@
package ac.mdiq.podcini.ui.compose
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.AppPreferences.getPref
import ac.mdiq.podcini.preferences.AppPreferences.putPref
import android.app.Activity
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
import ac.mdiq.podcini.preferences.SleepTimerPreferences.lastTimerValue
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setAutoEnable
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setAutoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setAutoEnableTo
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setLastTimer
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setShakeToReset
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setVibrate
import ac.mdiq.podcini.preferences.SleepTimerPreferences.shakeToReset
import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
import ac.mdiq.podcini.preferences.SleepTimerPreferences.vibrate
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.utils.DurationConverter.convertOnSpeed
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.ui.activity.MainActivity.Companion.lcScope
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.View
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@ -16,7 +41,6 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.graphics.Color
@ -26,7 +50,9 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
@ -34,9 +60,16 @@ import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.Popup
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
import kotlin.collections.set
import kotlin.math.max
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -287,15 +320,6 @@ fun MediaPlayerErrorDialog(activity: Context, message: String, showDialog: Mutab
}
}
@Composable
fun ComposableLifecycle(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onEvent: (LifecycleOwner, Lifecycle.Event) -> Unit) {
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { source, event -> onEvent(source, event) }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
}
@Composable
fun SearchBarRow(hintTextRes: Int, defaultText: String = "", performSearch: (String) -> Unit) {
val textColor = MaterialTheme.colorScheme.onSurface
@ -308,3 +332,143 @@ fun SearchBarRow(hintTextRes: Int, defaultText: String = "", performSearch: (Str
modifier = Modifier.width(40.dp).height(40.dp).padding(start = 5.dp).clickable(onClick = { performSearch(queryText) }))
}
}
@Composable
fun SleepTimerDialog(onDismiss: () -> Unit) {
val lcScope = rememberCoroutineScope()
val timeLeft = remember { (playbackService?.taskManager?.sleepTimerTimeLeft?:0) }
var showTimeDisplay by remember { mutableStateOf(false) }
var showTimeSetup by remember { mutableStateOf(true) }
var timerText by remember { mutableStateOf(getDurationStringLong(timeLeft.toInt())) }
fun timerUpdated(event: FlowEvent.SleepTimerUpdatedEvent) {
showTimeDisplay = !event.isOver && !event.isCancelled
showTimeSetup = event.isOver || event.isCancelled
timerText = getDurationStringLong(event.getTimeLeft().toInt())
}
var eventSink: Job? = remember { null }
fun cancelFlowEvents() {
eventSink?.cancel()
eventSink = null
}
fun procFlowEvents() {
if (eventSink != null) return
eventSink = lcScope.launch {
EventFlow.events.collectLatest { event ->
when (event) {
is FlowEvent.SleepTimerUpdatedEvent -> timerUpdated(event)
else -> {}
}
}
}
}
LaunchedEffect(Unit) { procFlowEvents() }
DisposableEffect(Unit) { onDispose { cancelFlowEvents() } }
var toEnd by remember { mutableStateOf(false) }
var etxtTime by remember { mutableStateOf(lastTimerValue()?:"") }
fun setSleepTimer(time: Long) {
playbackService?.taskManager?.setSleepTimer(time)
}
fun extendSleepTimer(extendTime: Long) {
val timeLeft = playbackService?.taskManager?.sleepTimerTimeLeft ?: Episode.INVALID_TIME.toLong()
if (timeLeft != Episode.INVALID_TIME.toLong()) setSleepTimer(timeLeft + extendTime)
}
val scrollState = rememberScrollState()
AlertDialog(modifier = Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)), onDismissRequest = onDismiss, title = { Text(stringResource(R.string.sleep_timer_label)) },
text = {
Column(modifier = Modifier.fillMaxWidth().verticalScroll(scrollState)) {
if (showTimeSetup) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp)) {
Checkbox(checked = toEnd, onCheckedChange = { toEnd = it })
Text(stringResource(R.string.end_episode), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 10.dp))
}
if (!toEnd) TextField(value = etxtTime, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Companion.Number), label = { Text(stringResource(R.string.time_minutes)) }, singleLine = true,
onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) etxtTime = it })
Button(modifier = Modifier.fillMaxWidth(), onClick = {
if (!PlaybackService.isRunning) {
// Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show()
return@Button
}
try {
val time = if (toEnd) {
val curPosition = curEpisode?.position ?: 0
val duration = curEpisode?.duration ?: 0
TimeUnit.MILLISECONDS.toMinutes(convertOnSpeed(max((duration - curPosition).toDouble(), 0.0).toInt(), curSpeedFB).toLong()) // ms to minutes
} else etxtTime.toLong()
Logd("SleepTimerDialog", "Sleep timer set: $time")
if (time == 0L) throw NumberFormatException("Timer must not be zero")
setLastTimer(time.toString())
setSleepTimer(timerMillis())
showTimeSetup = false
showTimeDisplay = true
// closeKeyboard(content)
} catch (e: NumberFormatException) {
e.printStackTrace()
// Snackbar.make(content, R.string.time_dialog_invalid_input, Snackbar.LENGTH_LONG).show()
}
}) { Text(stringResource(R.string.set_sleeptimer_label)) }
}
if (showTimeDisplay || timeLeft > 0) {
Text(timerText, style = MaterialTheme.typography.headlineMedium, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())
Button(modifier = Modifier.fillMaxWidth(), onClick = { playbackService?.taskManager?.disableSleepTimer()
}) { Text(stringResource(R.string.disable_sleeptimer_label)) }
Row {
Button(onClick = { extendSleepTimer((10 * 1000 * 60).toLong())
}) { Text(stringResource(R.string.extend_sleep_timer_label, 10)) }
Spacer(Modifier.weight(1f))
Button(onClick = { extendSleepTimer((30 * 1000 * 60).toLong())
}) { Text(stringResource(R.string.extend_sleep_timer_label, 30)) }
}
}
var cbShakeToReset by remember { mutableStateOf(shakeToReset()) }
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp)) {
Checkbox(checked = cbShakeToReset, onCheckedChange = {
cbShakeToReset = it
setShakeToReset(it)
})
Text(stringResource(R.string.shake_to_reset_label), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 10.dp))
}
var cbVibrate by remember { mutableStateOf(vibrate()) }
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp)) {
Checkbox(checked = cbVibrate, onCheckedChange = {
cbVibrate = it
setVibrate(it)
})
Text(stringResource(R.string.timer_vibration_label), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 10.dp))
}
var chAutoEnable by remember { mutableStateOf(autoEnable()) }
var enableChangeTime by remember { mutableStateOf(chAutoEnable) }
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp)) {
Checkbox(checked = chAutoEnable, onCheckedChange = {
chAutoEnable = it
setAutoEnable(it)
enableChangeTime = it
})
Text(stringResource(R.string.auto_enable_label), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 10.dp))
}
if (enableChangeTime) {
var from by remember { mutableStateOf(autoEnableFrom().toString()) }
var to by remember { mutableStateOf(autoEnableTo().toString()) }
Text(stringResource(R.string.auto_enable_sum), style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 10.dp))
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp).fillMaxWidth()) {
TextField(value = from, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Companion.Number),
label = { Text("From") }, singleLine = true, modifier = Modifier.weight(1f).padding(end = 8.dp),
onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) from = it })
TextField(value = to, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Companion.Number),
label = { Text("To") }, singleLine = true, modifier = Modifier.weight(1f).padding(end = 8.dp),
onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) to = it })
IconButton(onClick = {
setAutoEnableFrom(from.toInt())
setAutoEnableTo(to.toInt())
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings), contentDescription = "setting") }
}
}
}
},
confirmButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(R.string.close_label)) } }
)
}

View File

@ -1,382 +0,0 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.TimeDialogBinding
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
import ac.mdiq.podcini.preferences.SleepTimerPreferences.lastTimerValue
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setAutoEnable
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setAutoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setAutoEnableTo
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setLastTimer
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setShakeToReset
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setVibrate
import ac.mdiq.podcini.preferences.SleepTimerPreferences.shakeToReset
import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
import ac.mdiq.podcini.preferences.SleepTimerPreferences.vibrate
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.utils.DurationConverter.convertOnSpeed
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Point
import android.graphics.RectF
import android.os.Bundle
import android.text.format.DateFormat
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.*
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.*
class SleepTimerDialog : DialogFragment() {
private val TAG = "SleepTimerDialog"
private var _binding: TimeDialogBinding? = null
private val binding get() = _binding!!
private lateinit var etxtTime: EditText
private lateinit var chAutoEnable: CheckBox
override fun onStart() {
super.onStart()
procFlowEvents()
}
override fun onStop() {
super.onStop()
cancelFlowEvents()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = TimeDialogBinding.inflate(layoutInflater)
val content = binding.root
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(R.string.sleep_timer_label)
builder.setView(binding.root)
builder.setPositiveButton(R.string.close_label, null)
etxtTime = binding.etxtTime
binding.timeDisplay.visibility = View.GONE
val timeLeft = (playbackService?.taskManager?.sleepTimerTimeLeft?:0)
if (timeLeft > 0) {
binding.timeSetup.visibility = View.GONE
binding.timeDisplay.visibility = View.VISIBLE
binding.time.text = getDurationStringLong(timeLeft.toInt())
}
val extendSleepFiveMinutesButton = binding.extendSleepFiveMinutesButton
extendSleepFiveMinutesButton.text = getString(R.string.extend_sleep_timer_label, 5)
val extendSleepTenMinutesButton = binding.extendSleepTenMinutesButton
extendSleepTenMinutesButton.text = getString(R.string.extend_sleep_timer_label, 10)
val extendSleepTwentyMinutesButton = binding.extendSleepTwentyMinutesButton
extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20)
extendSleepFiveMinutesButton.setOnClickListener { extendSleepTimer((5 * 1000 * 60).toLong()) }
extendSleepTenMinutesButton.setOnClickListener { extendSleepTimer((10 * 1000 * 60).toLong()) }
extendSleepTwentyMinutesButton.setOnClickListener { extendSleepTimer((20 * 1000 * 60).toLong()) }
binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) etxtTime.visibility = View.GONE
else etxtTime.visibility = View.VISIBLE
}
etxtTime.setText(lastTimerValue())
etxtTime.postDelayed({
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT)
}, 100)
chAutoEnable = binding.chAutoEnable
val changeTimesButton = binding.changeTimesButton
binding.cbShakeToReset.isChecked = shakeToReset()
binding.cbVibrate.isChecked = vibrate()
chAutoEnable.setChecked(autoEnable())
changeTimesButton.isEnabled = chAutoEnable.isChecked
changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f
binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setShakeToReset(isChecked) }
binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setAutoEnable(isChecked)
changeTimesButton.isEnabled = isChecked
changeTimesButton.alpha = if (isChecked) 1.0f else 0.5f
}
updateAutoEnableText()
changeTimesButton.setOnClickListener {
val from = autoEnableFrom()
val to = autoEnableTo()
showTimeRangeDialog(from, to)
}
binding.disableSleeptimerButton.setOnClickListener { playbackService?.taskManager?.disableSleepTimer() }
binding.setSleeptimerButton.setOnClickListener {
if (!PlaybackService.isRunning) {
Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show()
return@setOnClickListener
}
try {
val time = if (binding.endEpisode.isChecked) {
val curPosition = curEpisode?.position ?: 0
val duration = curEpisode?.duration ?: 0
TimeUnit.MILLISECONDS.toMinutes(convertOnSpeed(max((duration - curPosition).toDouble(), 0.0).toInt(), curSpeedFB).toLong()) // ms to minutes
} else etxtTime.getText().toString().toLong()
Logd(TAG, "Sleep timer set: $time")
if (time == 0L) throw NumberFormatException("Timer must not be zero")
setLastTimer(time.toString())
setSleepTimer(timerMillis())
closeKeyboard(content)
} catch (e: NumberFormatException) {
e.printStackTrace()
Snackbar.make(content, R.string.time_dialog_invalid_input, Snackbar.LENGTH_LONG).show()
}
}
return builder.create()
}
override fun onDestroyView() {
Logd(TAG, "onDestroyView")
_binding = null
super.onDestroyView()
}
private fun extendSleepTimer(extendTime: Long) {
val timeLeft = playbackService?.taskManager?.sleepTimerTimeLeft ?: Episode.INVALID_TIME.toLong()
if (timeLeft != Episode.INVALID_TIME.toLong()) setSleepTimer(timeLeft + extendTime)
}
private fun setSleepTimer(time: Long) {
playbackService?.taskManager?.setSleepTimer(time)
}
private fun showTimeRangeDialog(from: Int, to: Int) {
val dialog = TimeRangeDialog(requireContext(), from, to)
dialog.setOnDismissListener {
setAutoEnableFrom(dialog.from)
setAutoEnableTo(dialog.to)
updateAutoEnableText()
}
dialog.show()
}
private fun updateAutoEnableText() {
val text: String
val from = autoEnableFrom()
val to = autoEnableTo()
when {
from == to -> text = getString(R.string.auto_enable_label)
DateFormat.is24HourFormat(context) -> {
val formattedFrom = String.format(Locale.getDefault(), "%02d:00", from)
val formattedTo = String.format(Locale.getDefault(), "%02d:00", to)
text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo)
}
else -> {
val formattedFrom = String.format(Locale.getDefault(), "%02d:00 %s", from % 12, if (from >= 12) "PM" else "AM")
val formattedTo = String.format(Locale.getDefault(), "%02d:00 %s", to % 12, if (to >= 12) "PM" else "AM")
text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo)
}
}
chAutoEnable.text = text
}
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.SleepTimerUpdatedEvent -> timerUpdated(event)
else -> {}
}
}
}
}
private fun timerUpdated(event: FlowEvent.SleepTimerUpdatedEvent) {
binding.timeDisplay.visibility = if (event.isOver || event.isCancelled) View.GONE else View.VISIBLE
binding.timeSetup.visibility = if (event.isOver || event.isCancelled) View.VISIBLE else View.GONE
binding.time.text = getDurationStringLong(event.getTimeLeft().toInt())
}
private fun closeKeyboard(content: View) {
val imm = requireContext().getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(content.windowToken, 0)
}
class TimeRangeDialog(context: Context, from: Int, to: Int) : MaterialAlertDialogBuilder(context) {
private val view = TimeRangeView(context, from, to)
val from: Int
get() = view.from
val to: Int
get() = view.to
init {
setView(view)
setPositiveButton(android.R.string.ok, null)
}
internal class TimeRangeView @JvmOverloads constructor(context: Context, internal var from: Int = 0, var to: Int = 0) : View(context) {
private val paintDial = Paint()
private val paintSelected = Paint()
private val paintText = Paint()
private val bounds = RectF()
private var touching: Int = 0
init {
setup()
}
private fun setup() {
paintDial.isAntiAlias = true
paintDial.style = Paint.Style.STROKE
paintDial.strokeCap = Paint.Cap.ROUND
paintDial.color = getColorFromAttr(context, android.R.attr.textColorPrimary)
paintDial.alpha = DIAL_ALPHA
paintSelected.isAntiAlias = true
paintSelected.style = Paint.Style.STROKE
paintSelected.strokeCap = Paint.Cap.ROUND
paintSelected.color = getColorFromAttr(context, androidx.appcompat.R.attr.colorAccent)
paintText.isAntiAlias = true
paintText.style = Paint.Style.FILL
paintText.color = getColorFromAttr(context, android.R.attr.textColorPrimary)
paintText.textAlign = Paint.Align.CENTER
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
when {
MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ->
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY -> super.onMeasure(widthMeasureSpec, widthMeasureSpec)
MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY -> super.onMeasure(heightMeasureSpec, heightMeasureSpec)
MeasureSpec.getSize(widthMeasureSpec) < MeasureSpec.getSize(heightMeasureSpec) -> super.onMeasure(widthMeasureSpec, widthMeasureSpec)
else -> super.onMeasure(heightMeasureSpec, heightMeasureSpec)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = height.toFloat() // square
val padding = size * 0.1f
paintDial.strokeWidth = size * 0.005f
bounds[padding, padding, size - padding] = size - padding
paintText.alpha = DIAL_ALPHA
canvas.drawArc(bounds, 0f, 360f, false, paintDial)
for (i in 0..23) {
paintDial.strokeWidth = size * 0.005f
if (i % 6 == 0) {
paintDial.strokeWidth = size * 0.01f
val textPos = radToPoint(i / 24.0f * 360f, size / 2 - 2.5f * padding)
paintText.textSize = 0.4f * padding
canvas.drawText(i.toString(), textPos.x.toFloat(), textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText)
}
val outer = radToPoint(i / 24.0f * 360f, size / 2 - 1.7f * padding)
val inner = radToPoint(i / 24.0f * 360f, size / 2 - 1.9f * padding)
canvas.drawLine(outer.x.toFloat(), outer.y.toFloat(), inner.x.toFloat(), inner.y.toFloat(), paintDial)
}
paintText.alpha = 255
val angleFrom = from.toFloat() / 24 * 360 - 90
val angleDistance = ((to - from + 24) % 24).toFloat() / 24 * 360
paintSelected.strokeWidth = padding / 6
paintSelected.style = Paint.Style.STROKE
canvas.drawArc(bounds, angleFrom, angleDistance, false, paintSelected)
paintSelected.style = Paint.Style.FILL
val p1 = radToPoint(angleFrom + 90, size / 2 - padding)
canvas.drawCircle(p1.x.toFloat(), p1.y.toFloat(), padding / 2, paintSelected)
val p2 = radToPoint(angleFrom + angleDistance + 90, size / 2 - padding)
canvas.drawCircle(p2.x.toFloat(), p2.y.toFloat(), padding / 2, paintSelected)
paintText.textSize = 0.6f * padding
val timeRange = when {
from == to -> context.getString(R.string.sleep_timer_always)
DateFormat.is24HourFormat(context) -> String.format(Locale.getDefault(), "%02d:00 - %02d:00", from, to)
else -> {
String.format(Locale.getDefault(), "%02d:00 %s - %02d:00 %s", from % 12,
if (from >= 12) "PM" else "AM", to % 12, if (to >= 12) "PM" else "AM")
}
}
canvas.drawText(timeRange, size / 2, (size - paintText.descent() - paintText.ascent()) / 2, paintText)
}
private fun radToPoint(angle: Float, radius: Float): Point {
return Point((width / 2 + radius * sin(-angle * Math.PI / 180 + Math.PI)).toInt(),
(height / 2 + radius * cos(-angle * Math.PI / 180 + Math.PI)).toInt())
}
override fun onTouchEvent(event: MotionEvent): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
val center = Point(width / 2, height / 2)
val angleRad = atan2((center.y - event.y).toDouble(), (center.x - event.x).toDouble())
var angle = (angleRad * (180 / Math.PI)).toFloat()
angle += (360 + 360 - 90).toFloat()
angle %= 360f
when {
event.action == MotionEvent.ACTION_DOWN -> {
val fromDistance = abs((angle - from.toFloat() / 24 * 360).toDouble()).toFloat()
val toDistance = abs((angle - to.toFloat() / 24 * 360).toDouble()).toFloat()
when {
fromDistance < 15 || fromDistance > (360 - 15) -> {
touching = 1
return true
}
toDistance < 15 || toDistance > (360 - 15) -> {
touching = 2
return true
}
}
}
event.action == MotionEvent.ACTION_MOVE -> {
val newTime = (24 * (angle / 360.0)).toInt()
// Switch which handle is focussed such that selection is the smaller arc
if (from == to && touching != 0) touching = if ((((newTime - to + 24) % 24) < 12)) 2 else 1
when (touching) {
1 -> {
from = newTime
invalidate()
return true
}
2 -> {
to = newTime
invalidate()
return true
}
}
}
touching != 0 -> {
touching = 0
return true
}
}
return super.onTouchEvent(event)
}
companion object {
private const val DIAL_ALPHA = 120
}
}
}
}

View File

@ -546,8 +546,6 @@ fun AudioPlayerScreen() {
modifier = Modifier.width(65.dp).height(65.dp).padding(start = 5.dp)
.clickable(onClick = {
Logd(TAG, "playerUi icon was clicked")
// vm.onExpanded()
// expandBottomSheet()
if (vm.isCollapsed) {
val media = curEpisode
if (media != null) {
@ -720,6 +718,9 @@ fun AudioPlayerScreen() {
}
}
var showSleepTimeDialog by remember { mutableStateOf(false) }
if (showSleepTimeDialog) SleepTimerDialog { showSleepTimeDialog = false }
@Composable
fun Toolbar() {
val context = LocalContext.current
@ -756,7 +757,7 @@ fun AudioPlayerScreen() {
if (vm.controller != null) {
val sleepRes = if (vm.sleepTimerActive) R.drawable.ic_sleep_off else R.drawable.ic_sleep
Icon(imageVector = ImageVector.vectorResource(sleepRes), tint = textColor, contentDescription = "Sleep timer", modifier = Modifier.clickable {
// TODO
showSleepTimeDialog = true
// SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
})
}

View File

@ -115,7 +115,7 @@ class EpisodesVM(val context: Context, val lcScope: CoroutineScope) {
for (url in event.urls) {
// if (!event.isCompleted(url)) continue
val pos: Int = Episodes.indexOfItemWithDownloadUrl(episodes, url)
if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
if (pos >= 0 && pos < vms.size) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}
}
@ -387,7 +387,7 @@ class EpisodesVM(val context: Context, val lcScope: CoroutineScope) {
val pos = Episodes.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes.removeAt(pos)
vms.removeAt(pos)
if (pos < vms.size) vms.removeAt(pos)
if (item.downloaded) {
episodes.add(pos, item)
vms.add(pos, EpisodeVM(item, TAG))
@ -407,7 +407,7 @@ class EpisodesVM(val context: Context, val lcScope: CoroutineScope) {
val pos = Episodes.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes.removeAt(pos)
vms.removeAt(pos)
if (pos < vms.size) vms.removeAt(pos)
if (item.downloaded) {
episodes.add(pos, item)
vms.add(pos, EpisodeVM(item, TAG))

View File

@ -123,8 +123,7 @@ class FeedEpisodesVM(val context: Context, val lcScope: CoroutineScope) {
if (feed != null) {
val pos: Int = ieMap[event.episode.id] ?: -1
if (pos >= 0) {
// TODO: vms[pos] may be out of bound
if (!isFilteredOut(event.episode)) vms[pos].isPlayingState = event.isPlaying()
if (!isFilteredOut(event.episode) && pos < vms.size) vms[pos].isPlayingState = event.isPlaying()
if (event.isPlaying()) upsertBlk(feed!!) { it.lastPlayed = Date().time }
}
}
@ -136,7 +135,7 @@ class FeedEpisodesVM(val context: Context, val lcScope: CoroutineScope) {
for (url in event.urls) {
// if (!event.isCompleted(url)) continue
val pos: Int = ueMap[url] ?: -1
if (pos >= 0) {
if (pos >= 0 && pos < vms.size) {
Logd(TAG, "onEpisodeDownloadEvent $pos ${event.map[url]?.state} ${episodes[pos].downloaded} ${episodes[pos].title}")
vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}

View File

@ -1,7 +1,6 @@
package ac.mdiq.podcini.ui.screens
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.EditTextDialogBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.net.feed.searcher.CombinedSearcher
import ac.mdiq.podcini.net.utils.HtmlToPlainText
@ -20,21 +19,13 @@ import ac.mdiq.podcini.ui.utils.feedOnDisplay
import ac.mdiq.podcini.ui.utils.setOnlineSearchTerms
import ac.mdiq.podcini.util.*
import ac.mdiq.podcini.util.MiscFormatter.fullDateTimeString
import android.R.string
import android.app.Activity
import android.content.*
import android.net.Uri
import android.os.CountDownTimer
import android.util.Log
import android.view.LayoutInflater
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
@ -61,12 +52,9 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import coil.compose.AsyncImage
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.apache.commons.lang3.StringUtils
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ExecutionException
@ -342,6 +330,52 @@ fun FeedInfoScreen() {
}
}
var showEidtConfirmDialog by remember { mutableStateOf(false) }
var editedUrl by remember { mutableStateOf("") }
@Composable
fun EditUrlSettingsDialog(onDismiss: () -> Unit) {
var url by remember { mutableStateOf(vm.feed.downloadUrl ?: "") }
AlertDialog(modifier = Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)), onDismissRequest = onDismiss, title = { Text(stringResource(R.string.edit_url_menu)) },
text = {
TextField(value = url, onValueChange = { url = it }, modifier = Modifier.fillMaxWidth())
},
confirmButton = {
TextButton(onClick = {
editedUrl = url
showEidtConfirmDialog = true
onDismiss()
}) { Text("OK") }
},
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel_label)) } }
)
}
@Composable
fun EidtConfirmDialog(onDismiss: () -> Unit) {
AlertDialog(modifier = Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)), onDismissRequest = onDismiss, title = { Text(stringResource(R.string.edit_url_menu)) },
text = {
Text(stringResource(R.string.edit_url_confirmation_msg))
},
confirmButton = {
TextButton(onClick = {
try {
runBlocking { updateFeedDownloadURL(vm.feed.downloadUrl?:"", editedUrl).join() }
vm.feed.downloadUrl = editedUrl
runOnce(context, vm.feed)
} catch (e: ExecutionException) { throw RuntimeException(e)
} catch (e: InterruptedException) { throw RuntimeException(e) }
vm.feed.downloadUrl = editedUrl
vm.txtvUrl = vm.feed.downloadUrl
onDismiss()
}) { Text("OK") }
},
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel_label)) } }
)
}
var showEditUrlSettingsDialog by remember { mutableStateOf(false) }
if (showEditUrlSettingsDialog) EditUrlSettingsDialog { showEditUrlSettingsDialog = false }
if (showEidtConfirmDialog) EidtConfirmDialog { showEidtConfirmDialog = false }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyTopAppBar() {
@ -362,12 +396,7 @@ fun FeedInfoScreen() {
expanded = false
})
if (!vm.feed.isLocalFeed) DropdownMenuItem(text = { Text(stringResource(R.string.edit_url_menu)) }, onClick = {
object : EditUrlSettingsDialog(context as Activity, vm.feed) {
override fun setUrl(url: String?) {
vm.feed.downloadUrl = url
vm.txtvUrl = vm.feed.downloadUrl
}
}.show()
showEditUrlSettingsDialog = true
expanded = false
})
DropdownMenuItem(text = { Text(stringResource(R.string.remove_feed_label)) }, onClick = {
@ -387,52 +416,4 @@ fun FeedInfoScreen() {
}
}
abstract class EditUrlSettingsDialog(activity: Activity, private val feed: Feed) {
private val activityRef = WeakReference(activity)
fun show() {
val activity = activityRef.get() ?: return
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
binding.editText.setText(feed.downloadUrl)
MaterialAlertDialogBuilder(activity)
.setView(binding.root)
.setTitle(R.string.edit_url_menu)
.setPositiveButton(string.ok) { _: DialogInterface?, _: Int -> showConfirmAlertDialog(binding.editText.text.toString()) }
.setNegativeButton(R.string.cancel_label, null)
.show()
}
private fun onConfirmed(original: String, updated: String) {
try {
runBlocking { updateFeedDownloadURL(original, updated).join() }
feed.downloadUrl = updated
runOnce(activityRef.get()!!, feed)
} catch (e: ExecutionException) { throw RuntimeException(e)
} catch (e: InterruptedException) { throw RuntimeException(e) }
}
private fun showConfirmAlertDialog(url: String) {
val activity = activityRef.get()
val alertDialog = MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.edit_url_menu)
.setMessage(R.string.edit_url_confirmation_msg)
.setPositiveButton(string.ok) { _: DialogInterface?, _: Int ->
onConfirmed(feed.downloadUrl?:"", url)
setUrl(url)
}
.setNegativeButton(R.string.cancel_label, null)
.show()
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
object : CountDownTimer(15000, 1000) {
override fun onTick(millisUntilFinished: Long) {
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).text = String.format(Locale.getDefault(), "%s (%d)",
activity.getString(string.ok), millisUntilFinished / 1000 + 1)
}
override fun onFinish() {
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(string.ok)
}
}.start()
}
protected abstract fun setUrl(url: String?)
}
private const val TAG: String = "FeedInfoScreen"

View File

@ -201,8 +201,10 @@ class QueuesVM(val context: Context, val lcScope: CoroutineScope) {
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
// queueItems[pos].stopMonitoring.value = true
queueItems.removeAt(pos)
vms[pos].stopMonitoring()
vms.removeAt(pos)
if (pos < vms.size) {
vms[pos].stopMonitoring()
vms.removeAt(pos)
}
} else {
Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}")
continue
@ -232,7 +234,7 @@ class QueuesVM(val context: Context, val lcScope: CoroutineScope) {
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
val pos: Int = Episodes.indexOfItemWithId(queueItems, event.episode.id)
Logd(TAG, "onPlayEvent action: ${event.action} pos: $pos ${event.episode.title}")
if (pos >= 0) vms[pos].isPlayingState = event.isPlaying()
if (pos >= 0 && pos < vms.size) vms[pos].isPlayingState = event.isPlaying()
}
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
@ -241,7 +243,7 @@ class QueuesVM(val context: Context, val lcScope: CoroutineScope) {
for (url in event.urls) {
// if (!event.isCompleted(url)) continue
val pos: Int = Episodes.indexOfItemWithDownloadUrl(queueItems.toList(), url)
if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
if (pos >= 0 && pos < vms.size) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}
}

View File

@ -20,6 +20,7 @@ import ac.mdiq.podcini.ui.activity.MainActivity.Screens
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.utils.curSearchString
import ac.mdiq.podcini.ui.utils.feedOnDisplay
import ac.mdiq.podcini.ui.utils.feedToSearchIn
import ac.mdiq.podcini.ui.utils.setOnlineFeedUrl
import ac.mdiq.podcini.ui.utils.setOnlineSearchTerms
import ac.mdiq.podcini.util.EventFlow
@ -87,6 +88,11 @@ class SearchVM(val context: Context, val lcScope: CoroutineScope) {
init {
queryText = curSearchString
if (feedToSearchIn != null) {
this.searchInFeed = true
feedId = feedToSearchIn!!.id
feedName = feedToSearchIn?.title ?: "Feed has no title"
}
automaticSearchDebouncer = Handler(Looper.getMainLooper())
swipeActions = SwipeActions(context, TAG)
leftActionState.value = swipeActions.actions.left[0]
@ -135,7 +141,7 @@ class SearchVM(val context: Context, val lcScope: CoroutineScope) {
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
for (url in event.urls) {
val pos: Int = Episodes.indexOfItemWithDownloadUrl(episodes, url)
if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
if (pos >= 0 && pos < vms.size) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}
}

View File

@ -3,24 +3,19 @@ package ac.mdiq.podcini.ui.utils
import ac.mdiq.podcini.net.feed.searcher.PodcastSearcher
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Feed
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation.NavController
import androidx.navigation.NavHostController
var episodeOnDisplay by mutableStateOf<Episode>(Episode())
var feedOnDisplay by mutableStateOf<Feed>(Feed())
var curSearchString by mutableStateOf("")
var searchInFeed by mutableStateOf<Feed?>(null)
var feedToSearchIn by mutableStateOf<Feed?>(null)
fun setSearchTerms(query: String, feed: Feed? = null) {
curSearchString = query
searchInFeed = feed
feedToSearchIn = feed
}
var onlineSearchText by mutableStateOf("")

View File

@ -1,15 +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:orientation="vertical"
android:padding="16dp">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:ems="10"
android:id="@+id/editText" />
</LinearLayout>

View File

@ -1,178 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
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="match_parent"
android:layout_height="wrap_content"
android:id="@+id/time_dialog">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<LinearLayout
android:id="@+id/timeSetup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<CheckBox
android:id="@+id/end_episode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/end_episode" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/etxtTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:selectAllOnFocus="true"
android:layout_margin="8dp"
android:ems="2"
android:inputType="number"
android:maxLength="3" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/time_minutes"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp" />
</LinearLayout>
<Button
android:id="@+id/setSleeptimerButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/set_sleeptimer_label" />
</LinearLayout>
<LinearLayout
android:id="@+id/timeDisplay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/time"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="00:00:00"
android:layout_gravity="center"
android:gravity="center"
android:textSize="32sp"
android:textColor="?android:attr/textColorPrimary"
tools:ignore="HardcodedText"/>
<Button
android:id="@+id/disableSleeptimerButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/disable_sleeptimer_label" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<Button
android:id="@+id/extendSleepFiveMinutesButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:paddingHorizontal="2dp"
android:paddingVertical="4dp"
android:layout_weight="1"
style="?attr/materialButtonOutlinedStyle"
tools:text="+5 min" />
<Button
android:id="@+id/extendSleepTenMinutesButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:paddingHorizontal="2dp"
android:paddingVertical="4dp"
android:layout_weight="1"
style="?attr/materialButtonOutlinedStyle"
tools:text="+10 min" />
<Button
android:id="@+id/extendSleepTwentyMinutesButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginRight="4dp"
android:layout_marginLeft="4dp"
android:paddingHorizontal="2dp"
android:paddingVertical="4dp"
android:layout_weight="1"
style="?attr/materialButtonOutlinedStyle"
tools:text="+20 min" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="8dp">
<CheckBox
android:id="@+id/cbShakeToReset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/shake_to_reset_label" />
<CheckBox
android:id="@+id/cbVibrate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/timer_vibration_label" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="1">
<CheckBox
android:id="@+id/chAutoEnable"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/auto_enable_label" />
<ImageView
android:id="@+id/changeTimesButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:contentDescription="@string/auto_enable_change_times"
android:background="?attr/selectableItemBackgroundBorderless"
app:srcCompat="@drawable/ic_settings" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -732,6 +732,7 @@
<string name="auto_enable_label_with_times" formatted="false">Automatically activate the sleep timer when pressing play between %s and %s</string>
<string name="auto_enable_change_times">Change time range</string>
<string name="sleep_timer_enabled_label">Sleep timer enabled</string>
<string name="auto_enable_sum">Enter the "From" hour and the "To" hour in the two boxes, then press the setting button. Hours are from 0 to 23.</string>
<!-- Synchronisation -->
<string name="wifi_sync">Sync with device on Wifi</string>

View File

@ -1,6 +1,7 @@
package ac.mdiq.podcini.playback.cast
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
@ -18,7 +19,7 @@ import com.google.android.gms.common.GoogleApiAvailability
/**
* Activity that allows for showing the MediaRouter button whenever there's a cast device in the network.
*/
abstract class CastEnabledActivity : AppCompatActivity() {
abstract class CastEnabledActivity : ComponentActivity() {
private var canCast by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -1,3 +1,11 @@
# 7.3.1
* likely fixed episodes list out of bound crashes
* fixed search in feed not being enabled
* sleep timer and url edit dialogs are in Compose
* all activities derive from ComponentActivity instead of AppCompatActivity
* androidx.fragment dependency removed
# 7.3.0
* last major step migrating to Jetpack Compose

View File

@ -0,0 +1,7 @@
Version 7.3.0
* likely fixed episodes list out of bound crashes
* fixed search in feed not being enabled
* sleep timer and url edit dialogs are in Compose
* all activities derive from ComponentActivity instead of AppCompatActivity
* androidx.fragment dependency removed