6.11.5 commit
This commit is contained in:
parent
ac412c3906
commit
ca6afe3c27
|
@ -16,7 +16,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
|||
That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs)
|
||||
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
|
||||
#### If you need to cast to an external speaker, you should install the "play" apk, not the "free" apk, that's about the difference between the two.
|
||||
#### Since version 6.8.5, Podcini.R is built to target SDK 30 (Android 11), though built with SDK 35 and tested on Android 14. This is to counter 2-year old Google issue ForegroundServiceStartNotAllowedException. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88)
|
||||
#### Since version 6.11.5, Podcini.R is back to be built to target SDK 35 (Android 15), but requests for permission for unrestricted background activities for uninterrupted background play of a playlist. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88)
|
||||
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
||||
|
||||
This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
|
||||
|
|
|
@ -20,7 +20,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdk 24
|
||||
compileSdk 35
|
||||
targetSdk 30
|
||||
targetSdk 35
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
|
@ -31,8 +31,8 @@ android {
|
|||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020274
|
||||
versionName "6.11.4"
|
||||
versionCode 3020275
|
||||
versionName "6.11.5"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
|
|
|
@ -441,10 +441,10 @@ object Feeds {
|
|||
return feed
|
||||
}
|
||||
|
||||
fun addToYoutubeSyndicate(episode: Episode, video: Boolean) {
|
||||
fun addToYoutubeSyndicate(episode: Episode, video: Boolean) : Int {
|
||||
val feed = getYoutubeSyndicate(video, episode.media?.downloadUrl?.contains("music") == true)
|
||||
Logd(TAG, "addToYoutubeSyndicate: feed: ${feed.title}")
|
||||
if (searchEpisodeByIdentifyingValue(feed.episodes, episode) != null) return
|
||||
if (searchEpisodeByIdentifyingValue(feed.episodes, episode) != null) return 2
|
||||
|
||||
Logd(TAG, "addToYoutubeSyndicate adding new episode: ${episode.title}")
|
||||
episode.feed = feed
|
||||
|
@ -455,6 +455,7 @@ object Feeds {
|
|||
feed.episodes.add(episode)
|
||||
upsertBlk(feed) {}
|
||||
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
|
||||
return 1
|
||||
}
|
||||
|
||||
fun createSynthetic(feedId: Long, name: String): Feed {
|
||||
|
|
|
@ -12,7 +12,7 @@ class ShareLog : RealmObject {
|
|||
|
||||
var type: String? = null
|
||||
|
||||
var status: Int = 0
|
||||
var status: Int = Status.ERROR.ordinal
|
||||
|
||||
var details: String = ""
|
||||
|
||||
|
@ -22,4 +22,10 @@ class ShareLog : RealmObject {
|
|||
id = Date().time
|
||||
this.url = url
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
ERROR,
|
||||
SUCCESS,
|
||||
EXISTING
|
||||
}
|
||||
}
|
|
@ -36,6 +36,7 @@ import ac.mdiq.podcini.util.FlowEvent
|
|||
import ac.mdiq.podcini.util.Logd
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AppOpsManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
@ -46,7 +47,9 @@ import android.media.AudioManager
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.StrictMode
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
|
@ -54,8 +57,11 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
|
@ -107,16 +113,70 @@ class MainActivity : CastEnabledActivity() {
|
|||
private var lastTheme = 0
|
||||
private var navigationBarInsets = Insets.NONE
|
||||
|
||||
// private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
// if (isGranted) return@registerForActivityResult
|
||||
// MaterialAlertDialogBuilder(this)
|
||||
// .setMessage(R.string.notification_permission_text)
|
||||
// .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||
// .setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||
// .show()
|
||||
// }
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) return@registerForActivityResult
|
||||
Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||
//
|
||||
// if (isGranted) return@registerForActivityResult
|
||||
|
||||
// MaterialAlertDialogBuilder(this)
|
||||
// .setMessage(R.string.notification_permission_text)
|
||||
// .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
// }
|
||||
// .setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int ->
|
||||
// }
|
||||
// .show()
|
||||
|
||||
if (isGranted) {
|
||||
checkAndRequestUnrestrictedBackgroundActivity(this)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
// checkAndRequestUnrestrictedBackgroundActivity(this)
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.notification_permission_text)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
checkAndRequestUnrestrictedBackgroundActivity(this)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int ->
|
||||
checkAndRequestUnrestrictedBackgroundActivity(this)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun checkAndRequestUnrestrictedBackgroundActivity(context: Context) {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val isIgnoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(context.packageName)
|
||||
|
||||
if (!isIgnoringBatteryOptimizations) {
|
||||
// Toast.makeText(context, "Please allow unrestricted background activity for this app", Toast.LENGTH_LONG).show()
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.unrestricted_background_permission_text)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
var intent = Intent()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
intent.action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
// intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).also {
|
||||
// val uri = Uri.parse("package:$packageName")
|
||||
// it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
// it.data = uri
|
||||
// }
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private var prevState: Int = 0
|
||||
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
|
||||
override fun onStateChanged(view: View, state: Int) {
|
||||
|
@ -200,10 +260,17 @@ class MainActivity : CastEnabledActivity() {
|
|||
mainView = findViewById(R.id.main_view)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
// Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||
// requestPostNotificationPermission()
|
||||
// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.notification_permission_text)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int ->
|
||||
checkAndRequestUnrestrictedBackgroundActivity(this@MainActivity)
|
||||
}
|
||||
.show()
|
||||
} else checkAndRequestUnrestrictedBackgroundActivity(this)
|
||||
|
||||
// Consume navigation bar insets - we apply them in setPlayerVisible()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(mainView) { _: View?, insets: WindowInsetsCompat ->
|
||||
|
@ -750,6 +817,9 @@ class MainActivity : CastEnabledActivity() {
|
|||
const val MAIN_FRAGMENT_TAG: String = "main"
|
||||
const val PREF_NAME: String = "MainActivityPrefs"
|
||||
|
||||
const val REQUEST_CODE_FIRST_PERMISSION = 1001
|
||||
const val REQUEST_CODE_SECOND_PERMISSION = 1002
|
||||
|
||||
@JvmStatic
|
||||
fun getIntentToOpenFeed(context: Context, feedId: Long): Intent {
|
||||
val intent = Intent(context.applicationContext, MainActivity::class.java)
|
||||
|
|
|
@ -11,7 +11,6 @@ import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfo
|
|||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Feeds.addToMiscSyndicate
|
||||
import ac.mdiq.podcini.storage.database.Feeds.addToYoutubeSyndicate
|
||||
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
|
||||
|
@ -80,7 +79,6 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import io.realm.kotlin.notifications.SingleQueryChange
|
||||
import io.realm.kotlin.notifications.UpdatedObject
|
||||
|
@ -175,6 +173,7 @@ class EpisodeVM(var episode: Episode) {
|
|||
inProgressState = changes.obj.isInProgress
|
||||
Logd("EpisodeVM", "mediaMonitor $positionState $inProgressState ${episode.title}")
|
||||
episode = changes.obj
|
||||
// Logd("EpisodeVM", "mediaMonitor downloaded: ${changes.obj.media?.downloaded} ${episode.media?.downloaded}")
|
||||
}
|
||||
} else Logd("EpisodeVM", "mediaMonitor index out bound")
|
||||
}
|
||||
|
@ -191,7 +190,7 @@ fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (rating in Rating.entries) {
|
||||
for (rating in Rating.entries.reversed()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
||||
for (item in selected) Episodes.setRating(item, rating.code)
|
||||
onDismissRequest()
|
||||
|
@ -557,8 +556,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
val velocityTracker = remember { VelocityTracker() }
|
||||
val offsetX = remember { Animatable(0f) }
|
||||
Box(modifier = Modifier.fillMaxWidth().pointerInput(Unit) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = { velocityTracker.resetTracking() },
|
||||
detectHorizontalDragGestures(onDragStart = { velocityTracker.resetTracking() },
|
||||
onHorizontalDrag = { change, dragAmount ->
|
||||
velocityTracker.addPosition(change.uptimeMillis, change.position)
|
||||
coroutineScope.launch { offsetX.snapTo(offsetX.value + dragAmount) }
|
||||
|
@ -573,10 +571,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
if (velocity > 0) rightSwipeCB?.invoke(vm.episode)
|
||||
else leftSwipeCB?.invoke(vm.episode)
|
||||
}
|
||||
offsetX.animateTo(
|
||||
targetValue = 0f, // Back to the initial position
|
||||
animationSpec = tween(500) // Adjust animation duration as needed
|
||||
)
|
||||
offsetX.animateTo(targetValue = 0f, animationSpec = tween(500))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -592,21 +587,21 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
else selected.remove(vms[index].episode)
|
||||
}
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Row (Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
|
||||
Column {
|
||||
val dur = vm.episode.media?.getDuration() ?: 0
|
||||
val durText = DurationConverter.getDurationStringLong(dur)
|
||||
Row(Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
|
||||
if (false) {
|
||||
val typedValue = TypedValue()
|
||||
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
|
||||
Icon(painter = painterResource(typedValue.resourceId), tint = textColor,
|
||||
contentDescription = "drag handle",
|
||||
Icon(painter = painterResource(typedValue.resourceId), tint = textColor, contentDescription = "drag handle",
|
||||
modifier = Modifier.width(16.dp).align(Alignment.CenterVertically))
|
||||
}
|
||||
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
|
||||
val (imgvCover, checkMark) = createRefs()
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode)
|
||||
val painter = rememberAsyncImagePainter(model = imgLoc)
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = "imgvCover",
|
||||
Image(painter = painter, contentDescription = "imgvCover",
|
||||
modifier = Modifier.width(56.dp).height(56.dp)
|
||||
.constrainAs(imgvCover) {
|
||||
top.linkTo(parent.top)
|
||||
|
@ -619,7 +614,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
})
|
||||
)
|
||||
val alpha = if (vm.playedState) 1.0f else 0f
|
||||
if (vm.playedState) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark",
|
||||
if (vm.playedState) Icon(painter = painterResource(R.drawable.ic_check),
|
||||
tint = textColor,
|
||||
contentDescription = "played_mark",
|
||||
modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(parent.end)
|
||||
|
@ -644,48 +641,40 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
Logd(TAG, "long clicked: ${vm.episode.title}")
|
||||
})) {
|
||||
LaunchedEffect(key1 = queueChanged) {
|
||||
if (index>=vms.size) return@LaunchedEffect
|
||||
if (index >= vms.size) return@LaunchedEffect
|
||||
vms[index].inQueueState = curQueue.contains(vms[index].episode)
|
||||
}
|
||||
val dur = vm.episode.media?.getDuration() ?: 0
|
||||
val durText = DurationConverter.getDurationStringLong(dur)
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (vm.episode.media?.getMediaType() == MediaType.VIDEO)
|
||||
Icon(painter = painterResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
Icon(painter = painterResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo",
|
||||
modifier = Modifier.width(14.dp).height(14.dp))
|
||||
val ratingIconRes = Rating.fromCode(vm.ratingState).res
|
||||
if (vm.ratingState != Rating.UNRATED.code)
|
||||
Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(14.dp).height(14.dp))
|
||||
Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating",
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(14.dp).height(14.dp))
|
||||
if (vm.inQueueState)
|
||||
Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist",
|
||||
modifier = Modifier.width(14.dp).height(14.dp))
|
||||
val curContext = LocalContext.current
|
||||
val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " + if((vm.episode.media?.size?:0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
|
||||
val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " +
|
||||
if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
|
||||
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Text(vm.episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
if (InTheatre.isCurMedia(vm.episode.media) || vm.inProgressState) {
|
||||
val pos = vm.positionState
|
||||
vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
|
||||
Logd(TAG, "$index vm.prog: ${vm.prog}")
|
||||
Row {
|
||||
Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
|
||||
Text(durText, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
Text(vm.episode.title ?: "", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
fun isDownloading(): Boolean {
|
||||
return vms[index].downloadState > DownloadStatus.State.UNKNOWN.ordinal && vms[index].downloadState < DownloadStatus.State.COMPLETED.ordinal
|
||||
}
|
||||
if (actionButton_ == null) {
|
||||
LaunchedEffect(vms[index].downloadState) {
|
||||
if (index>=vms.size) return@LaunchedEffect
|
||||
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media?.downloadUrl?:"") ?: 0
|
||||
if (index >= vms.size) return@LaunchedEffect
|
||||
if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media?.downloadUrl ?: "") ?: 0
|
||||
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
|
||||
vm.actionButton = EpisodeActionButton.forItem(vm.episode)
|
||||
vm.actionRes = vm.actionButton!!.getDrawable()
|
||||
}
|
||||
LaunchedEffect(key1 = status) {
|
||||
if (index>=vms.size) return@LaunchedEffect
|
||||
if (index >= vms.size) return@LaunchedEffect
|
||||
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vms[index].episode.title}")
|
||||
vm.actionButton = EpisodeActionButton.forItem(vm.episode)
|
||||
Logd(TAG, "LaunchedEffect vm.actionButton: ${vm.actionButton?.getLabel()}")
|
||||
|
@ -697,16 +686,30 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
// vms[index].actionRes = vm.actionButton.getDrawable()
|
||||
// }
|
||||
}
|
||||
Box(modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.CenterVertically).pointerInput(Unit) {
|
||||
Box(modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp)
|
||||
.align(Alignment.CenterVertically).pointerInput(Unit) {
|
||||
detectTapGestures(onLongPress = { vm.showAltActionsDialog = true }, onTap = {
|
||||
vms[index].actionButton?.onClick(activity)
|
||||
})
|
||||
}, contentAlignment = Alignment.Center) {
|
||||
// actionRes = actionButton.getDrawable()
|
||||
Icon(painter = painterResource(vm.actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
|
||||
if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent}, strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp))
|
||||
if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent },
|
||||
strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp))
|
||||
}
|
||||
if (vm.showAltActionsDialog) vm.actionButton?.AltActionsDialog(activity, vm.showAltActionsDialog,
|
||||
onDismiss = { vm.showAltActionsDialog = false })
|
||||
}
|
||||
if (InTheatre.isCurMedia(vm.episode.media) || vm.inProgressState) {
|
||||
val pos = vm.positionState
|
||||
vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
|
||||
Logd(TAG, "$index vm.prog: ${vm.prog}")
|
||||
Row {
|
||||
Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
|
||||
Text(durText, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
if (vm.showAltActionsDialog) vm.actionButton?.AltActionsDialog(activity, vm.showAltActionsDialog, onDismiss = { vm.showAltActionsDialog = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -780,8 +783,8 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
|
|||
try {
|
||||
val info = StreamInfo.getInfo(Vista.getService(0), url)
|
||||
val episode = episodeFromStreamInfo(info)
|
||||
addToYoutubeSyndicate(episode, !audioOnly)
|
||||
if (log != null) upsert(log) { it.status = 1 }
|
||||
val status = addToYoutubeSyndicate(episode, !audioOnly)
|
||||
if (log != null) upsert(log) { it.status = status }
|
||||
} catch (e: Throwable) {
|
||||
toastMassege = "Receive share error: ${e.message}"
|
||||
Log.e(TAG, toastMassege)
|
||||
|
|
|
@ -86,7 +86,7 @@ import java.util.concurrent.Semaphore
|
|||
private var feedID: Long = 0
|
||||
private var feed by mutableStateOf<Feed?>(null)
|
||||
|
||||
private val episodes = mutableListOf<Episode>()
|
||||
private val episodes = mutableStateListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
|
||||
private var ieMap: Map<Long, Int> = mapOf()
|
||||
|
@ -159,7 +159,7 @@ import java.util.concurrent.Semaphore
|
|||
loadItemsRunning = false
|
||||
}
|
||||
}
|
||||
binding.lazyColumn.setContent {
|
||||
binding.mainView.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
if (showRemoveFeedDialog) RemoveFeedDialog(listOf(feed!!), onDismissRequest = {showRemoveFeedDialog = false}) {
|
||||
(activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
|
||||
|
@ -248,7 +248,7 @@ import java.util.concurrent.Semaphore
|
|||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.weight(0.7f))
|
||||
Icon(painter = painterResource(R.drawable.ic_filter_white), tint = if (filterButColor == Color.White) textColor else filterButColor, contentDescription = "butFilter",
|
||||
modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).combinedClickable(onClick = filterClickCB, onLongClick = filterLongClickCB))
|
||||
Spacer(modifier = Modifier.width(15.dp))
|
||||
|
@ -259,8 +259,8 @@ import java.util.concurrent.Semaphore
|
|||
activity.loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||
}
|
||||
}))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(feed?.episodes?.size?.toString()?:"", textAlign = TextAlign.Center, color = Color.White, style = MaterialTheme.typography.bodyLarge)
|
||||
Spacer(modifier = Modifier.weight(0.5f))
|
||||
Text(episodes.size.toString() + " / " + feed?.episodes?.size?.toString(), textAlign = TextAlign.Center, color = Color.White, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
// Image(painter = painterResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner",
|
||||
// Modifier.width(12.dp).height(12.dp).constrainAs(image1) {
|
||||
|
|
|
@ -174,7 +174,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
Spacer(modifier = Modifier.weight(1f))
|
||||
val ratingIconRes = Rating.fromCode(rating).res
|
||||
Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating",
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(24.dp).height(24.dp).clickable(onClick = {
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(30.dp).height(30.dp).clickable(onClick = {
|
||||
showChooseRatingDialog = true
|
||||
}))
|
||||
Spacer(modifier = Modifier.weight(0.2f))
|
||||
|
|
|
@ -3,6 +3,7 @@ package ac.mdiq.podcini.ui.fragment
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.LogsFragmentBinding
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
|
@ -101,10 +102,10 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@Composable
|
||||
fun SharedLogView() {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
val dialogParam = remember { mutableStateOf(ShareLog()) }
|
||||
if (showDialog.value) {
|
||||
SharedDetailDialog(status = dialogParam.value, showDialog = showDialog.value, onDismissRequest = { showDialog.value = false })
|
||||
val showSharedDialog = remember { mutableStateOf(false) }
|
||||
val sharedlogState = remember { mutableStateOf(ShareLog()) }
|
||||
if (showSharedDialog.value) {
|
||||
SharedDetailDialog(status = sharedlogState.value, showDialog = showSharedDialog.value, onDismissRequest = { showSharedDialog.value = false })
|
||||
}
|
||||
var showYTMediaConfirmDialog by remember { mutableStateOf(false) }
|
||||
var sharedUrl by remember { mutableStateOf("") }
|
||||
|
@ -116,24 +117,29 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
itemsIndexed(shareLogs) { position, log ->
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Row (modifier = Modifier.clickable {
|
||||
if (log.status == 1) {
|
||||
showDialog.value = true
|
||||
dialogParam.value = log
|
||||
} else {
|
||||
if (log.status < ShareLog.Status.SUCCESS.ordinal) {
|
||||
receiveShared(log.url!!, activity as AppCompatActivity, false) {
|
||||
sharedUrl = log.url!!
|
||||
showYTMediaConfirmDialog = true
|
||||
}
|
||||
} else {
|
||||
Logd(TAG, "shared log url: ${log.url}")
|
||||
// val episode = getEpisodeByGuidOrUrl(null, log.url!!, false)
|
||||
// if (episode != null) (activity as MainActivity).loadChildFragment(EpisodeInfoFragment.newInstance(episode))
|
||||
// else {
|
||||
showSharedDialog.value = true
|
||||
sharedlogState.value = log
|
||||
// }
|
||||
}
|
||||
}) {
|
||||
Column {
|
||||
Row {
|
||||
val icon = remember { if (log.status == 1) Icons.Filled.Info else Icons.Filled.Warning }
|
||||
val iconColor = remember { if (log.status == 1) Color.Green else Color.Yellow }
|
||||
val icon = remember { if (log.status == ShareLog.Status.SUCCESS.ordinal) Icons.Filled.Info else Icons.Filled.Warning }
|
||||
val iconColor = remember { if (log.status == ShareLog.Status.SUCCESS.ordinal) Color.Green else Color.Yellow }
|
||||
Icon(icon, "Info", tint = iconColor, modifier = Modifier.padding(end = 2.dp))
|
||||
Text(formatDateTimeFlex(Date(log.id)), color = textColor)
|
||||
Spacer(Modifier.weight(1f))
|
||||
var showAction by remember { mutableStateOf(log.status != 1) }
|
||||
var showAction by remember { mutableStateOf(log.status < ShareLog.Status.SUCCESS.ordinal) }
|
||||
if (true || showAction) {
|
||||
Icon(painter = painterResource(R.drawable.ic_delete), tint = textColor, contentDescription = null,
|
||||
modifier = Modifier.width(25.dp).height(25.dp).clickable {
|
||||
|
@ -141,9 +147,18 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
}
|
||||
Text(log.url?:"unknown", color = textColor)
|
||||
val statusText = remember {"" }
|
||||
val statusText = when (log.status) {
|
||||
ShareLog.Status.ERROR.ordinal -> ShareLog.Status.ERROR.name
|
||||
ShareLog.Status.SUCCESS.ordinal -> ShareLog.Status.SUCCESS.name
|
||||
ShareLog.Status.EXISTING.ordinal -> ShareLog.Status.EXISTING.name
|
||||
else -> ""
|
||||
}
|
||||
Row {
|
||||
Text(statusText, color = textColor)
|
||||
if (log.status != 1) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(log.type?:"unknow type", color = textColor)
|
||||
}
|
||||
if (log.status < ShareLog.Status.SUCCESS.ordinal) {
|
||||
Text(log.details, color = Color.Red)
|
||||
Text(stringResource(R.string.download_error_tap_for_details), color = textColor)
|
||||
}
|
||||
|
@ -355,9 +370,12 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@Composable
|
||||
fun SharedDetailDialog(status: ShareLog, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
var message = requireContext().getString(R.string.download_successful)
|
||||
if (status.status == 0) message = status.details
|
||||
|
||||
val message = when (status.status) {
|
||||
ShareLog.Status.ERROR.ordinal -> status.details
|
||||
ShareLog.Status.SUCCESS.ordinal -> stringResource(R.string.download_successful)
|
||||
ShareLog.Status.EXISTING.ordinal -> stringResource(R.string.share_existing)
|
||||
else -> ""
|
||||
}
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
|
|
|
@ -33,7 +33,6 @@ import ac.mdiq.podcini.util.Logd
|
|||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import android.app.Activity
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
@ -86,8 +85,6 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
@ -125,7 +122,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
private var feedCount by mutableStateOf("")
|
||||
private var feedSorted by mutableIntStateOf(0)
|
||||
|
||||
private var feedList: MutableList<Feed> = mutableListOf()
|
||||
// private var feedList: MutableList<Feed> = mutableListOf()
|
||||
private var feedListFiltered = mutableStateListOf<Feed>()
|
||||
|
||||
private var useGrid by mutableStateOf<Boolean?>(null)
|
||||
|
@ -200,7 +197,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
feedCount = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
feedCount = feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString()
|
||||
loadSubscriptions()
|
||||
return binding.root
|
||||
}
|
||||
|
@ -224,7 +221,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
feedList = mutableListOf()
|
||||
// feedList = mutableListOf()
|
||||
feedListFiltered.clear()
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
|
@ -345,10 +342,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (!loadItemsRunning) {
|
||||
loadItemsRunning = true
|
||||
lifecycleScope.launch {
|
||||
val feedList: List<Feed>
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
resetTags()
|
||||
filterAndSort()
|
||||
feedList = filterAndSort()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
// We have fewer items. This can result in items being selected that are no longer visible.
|
||||
|
@ -356,7 +354,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
// filterOnTag()
|
||||
feedListFiltered.clear()
|
||||
feedListFiltered.addAll(feedList)
|
||||
feedCount = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
feedCount = feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString()
|
||||
infoTextFiltered = " "
|
||||
if (feedsFilter.isNotEmpty()) infoTextFiltered = getString(R.string.filtered_label)
|
||||
txtvInformation = (infoTextFiltered + infoTextUpdate)
|
||||
|
@ -368,7 +366,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
}
|
||||
|
||||
private fun filterAndSort() {
|
||||
private fun filterAndSort(): List<Feed> {
|
||||
var fQueryStr = FeedFilter(feedsFilter).queryString()
|
||||
val tagsQueryStr = queryStringOfTags()
|
||||
if (tagsQueryStr.isNotEmpty()) fQueryStr += " AND $tagsQueryStr"
|
||||
|
@ -478,8 +476,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
comparator(counterMap, dir)
|
||||
}
|
||||
}
|
||||
synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() }
|
||||
// synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() }
|
||||
feedSorted++
|
||||
return feedList_.sortedWith(comparator)
|
||||
}
|
||||
|
||||
private fun comparator(counterMap: Map<Long, Long>, dir: Int): Comparator<Feed> {
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/lazyColumn"
|
||||
android:id="@+id/mainView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/show_download_logs"
|
||||
android:title="@string/show_download_logs_label"
|
||||
android:icon="@drawable/ic_download"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/show_shared_logs"
|
||||
android:title="@string/show_shared_logs_label"
|
||||
|
@ -15,12 +21,6 @@
|
|||
android:icon="@drawable/ic_subscriptions"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/show_download_logs"
|
||||
android:title="@string/show_download_logs_label"
|
||||
android:icon="@drawable/ic_download"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/clear_logs_item"
|
||||
android:title="@string/clear_history_label"
|
||||
|
|
|
@ -67,6 +67,8 @@
|
|||
<string name="tts_not_available">TTS engine not available</string>
|
||||
<string name="episode_has_no_content">Episode has not content</string>
|
||||
|
||||
<string name="unrestricted_background_permission_text">In order to successfully play a list of episodes in the background, Podcini needs Permission for Unrestricted Background Activity. Note on some brands, there are sustomized settings for this, please ensure to set them accordingly.</string>
|
||||
|
||||
<string name="notification_permission_text">Since Android 13, top level notification is needed for normal refresh and playback. You may disallow notifications of sub-catergories at your wish.</string>
|
||||
<string name="notification_permission_denied">You denied the permission.</string>
|
||||
<string name="notification_permission_deny_warning">If you disable notifications and something goes wrong, you might be unable to find out why it went wrong.</string>
|
||||
|
@ -312,6 +314,8 @@
|
|||
<string name="no_items_selected">No items selected</string>
|
||||
<string name="delete_local_feed_warning_body">Warning: you are deleting local episodes. It will delete the media files from your device storage. It cannot be downloaded again through Podcini. Continue?</string>
|
||||
|
||||
<string name="share_existing">existing</string>
|
||||
|
||||
<!-- Download messages and labels -->
|
||||
<string name="download_successful">successful</string>
|
||||
<string name="download_pending">Download pending</string>
|
||||
|
|
12
changelog.md
12
changelog.md
|
@ -1,3 +1,15 @@
|
|||
# 6.11.5
|
||||
|
||||
* back to be built to target Android 15
|
||||
* request for permission for unrestricted background activity
|
||||
* requests for permission are now less aggressive: if cancelled, Podcini does not quit
|
||||
* reversed rating list in popup (favorite on top)
|
||||
* if a shared Youtube media already exists, record shared log as such
|
||||
* in episodes list, made the progress bar taking the full width
|
||||
* in FeedEpisodes header, the count now reflects filter
|
||||
* in Subscriptions, feed count now reflects filter
|
||||
* minor layout adjustments
|
||||
|
||||
# 6.11.4
|
||||
|
||||
* corrected color contrast in SwipeActions dialog
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
Version 6.11.5
|
||||
|
||||
* back to be built to target Android 15
|
||||
* request for permission for unrestricted background activity
|
||||
* requests for permission are now less aggressive: if cancelled, Podcini does not quit
|
||||
* reversed rating list in popup (favorite on top)
|
||||
* if a shared Youtube media already exists, record shared log as such
|
||||
* in episodes list, made the progress bar taking the full width
|
||||
* in FeedEpisodes header, the count now reflects filter
|
||||
* in Subscriptions, feed count now reflects filter
|
||||
* minor layout adjustments
|
Loading…
Reference in New Issue