mirror of
https://github.com/XilinJia/Podcini.git
synced 2025-02-09 07:58:47 +01:00
7.3.1 commit
This commit is contained in:
parent
6e26b80488
commit
d913f2474d
@ -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.
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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("")
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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?) {
|
||||
|
@ -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 = {
|
||||
|
@ -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)) } }
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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("")
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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?) {
|
||||
|
@ -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
|
||||
|
7
fastlane/metadata/android/en-US/changelogs/3020338.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/3020338.txt
Normal 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
|
Loading…
x
Reference in New Issue
Block a user