6.11.5 commit

This commit is contained in:
Xilin Jia 2024-10-18 17:47:44 +01:00
parent ac412c3906
commit ca6afe3c27
16 changed files with 292 additions and 167 deletions

View File

@ -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) 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. #### 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. #### 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. #### 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. This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.

View File

@ -20,7 +20,7 @@ android {
defaultConfig { defaultConfig {
minSdk 24 minSdk 24
compileSdk 35 compileSdk 35
targetSdk 30 targetSdk 35
kotlinOptions { kotlinOptions {
jvmTarget = '17' jvmTarget = '17'
@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020274 versionCode 3020275
versionName "6.11.4" versionName "6.11.5"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -16,6 +16,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<supports-screens <supports-screens
android:anyDensity="true" android:anyDensity="true"

View File

@ -441,10 +441,10 @@ object Feeds {
return feed 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) val feed = getYoutubeSyndicate(video, episode.media?.downloadUrl?.contains("music") == true)
Logd(TAG, "addToYoutubeSyndicate: feed: ${feed.title}") 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}") Logd(TAG, "addToYoutubeSyndicate adding new episode: ${episode.title}")
episode.feed = feed episode.feed = feed
@ -455,6 +455,7 @@ object Feeds {
feed.episodes.add(episode) feed.episodes.add(episode)
upsertBlk(feed) {} upsertBlk(feed) {}
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false)) EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
return 1
} }
fun createSynthetic(feedId: Long, name: String): Feed { fun createSynthetic(feedId: Long, name: String): Feed {

View File

@ -12,7 +12,7 @@ class ShareLog : RealmObject {
var type: String? = null var type: String? = null
var status: Int = 0 var status: Int = Status.ERROR.ordinal
var details: String = "" var details: String = ""
@ -22,4 +22,10 @@ class ShareLog : RealmObject {
id = Date().time id = Date().time
this.url = url this.url = url
} }
enum class Status {
ERROR,
SUCCESS,
EXISTING
}
} }

View File

@ -36,6 +36,7 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
@ -46,7 +47,9 @@ import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager
import android.os.StrictMode import android.os.StrictMode
import android.provider.Settings
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
@ -54,8 +57,11 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.widget.EditText import android.widget.EditText
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -107,16 +113,70 @@ class MainActivity : CastEnabledActivity() {
private var lastTheme = 0 private var lastTheme = 0
private var navigationBarInsets = Insets.NONE 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 -> 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) MaterialAlertDialogBuilder(this)
.setMessage(R.string.notification_permission_text) .setMessage(R.string.notification_permission_text)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> } .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() } checkAndRequestUnrestrictedBackgroundActivity(this)
}
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int ->
checkAndRequestUnrestrictedBackgroundActivity(this)
}
.show() .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 var prevState: Int = 0
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() { private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
override fun onStateChanged(view: View, state: Int) { override fun onStateChanged(view: View, state: Int) {
@ -200,10 +260,17 @@ class MainActivity : CastEnabledActivity() {
mainView = findViewById(R.id.main_view) mainView = findViewById(R.id.main_view)
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { 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() // requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
// requestPostNotificationPermission() MaterialAlertDialogBuilder(this)
.setMessage(R.string.notification_permission_text)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 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() // Consume navigation bar insets - we apply them in setPlayerVisible()
ViewCompat.setOnApplyWindowInsetsListener(mainView) { _: View?, insets: WindowInsetsCompat -> ViewCompat.setOnApplyWindowInsetsListener(mainView) { _: View?, insets: WindowInsetsCompat ->
@ -750,6 +817,9 @@ class MainActivity : CastEnabledActivity() {
const val MAIN_FRAGMENT_TAG: String = "main" const val MAIN_FRAGMENT_TAG: String = "main"
const val PREF_NAME: String = "MainActivityPrefs" const val PREF_NAME: String = "MainActivityPrefs"
const val REQUEST_CODE_FIRST_PERMISSION = 1001
const val REQUEST_CODE_SECOND_PERMISSION = 1002
@JvmStatic @JvmStatic
fun getIntentToOpenFeed(context: Context, feedId: Long): Intent { fun getIntentToOpenFeed(context: Context, feedId: Long): Intent {
val intent = Intent(context.applicationContext, MainActivity::class.java) val intent = Intent(context.applicationContext, MainActivity::class.java)

View File

@ -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.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.Feeds.addToMiscSyndicate import ac.mdiq.podcini.storage.database.Feeds.addToMiscSyndicate
import ac.mdiq.podcini.storage.database.Feeds.addToYoutubeSyndicate 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
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet 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.compose.ui.window.Dialog
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import io.realm.kotlin.notifications.SingleQueryChange import io.realm.kotlin.notifications.SingleQueryChange
import io.realm.kotlin.notifications.UpdatedObject import io.realm.kotlin.notifications.UpdatedObject
@ -175,6 +173,7 @@ class EpisodeVM(var episode: Episode) {
inProgressState = changes.obj.isInProgress inProgressState = changes.obj.isInProgress
Logd("EpisodeVM", "mediaMonitor $positionState $inProgressState ${episode.title}") Logd("EpisodeVM", "mediaMonitor $positionState $inProgressState ${episode.title}")
episode = changes.obj episode = changes.obj
// Logd("EpisodeVM", "mediaMonitor downloaded: ${changes.obj.media?.downloaded} ${episode.media?.downloaded}")
} }
} else Logd("EpisodeVM", "mediaMonitor index out bound") } else Logd("EpisodeVM", "mediaMonitor index out bound")
} }
@ -191,7 +190,7 @@ fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) { Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp)) { Surface(shape = RoundedCornerShape(16.dp)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(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 { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
for (item in selected) Episodes.setRating(item, rating.code) for (item in selected) Episodes.setRating(item, rating.code)
onDismissRequest() onDismissRequest()
@ -557,8 +556,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
val velocityTracker = remember { VelocityTracker() } val velocityTracker = remember { VelocityTracker() }
val offsetX = remember { Animatable(0f) } val offsetX = remember { Animatable(0f) }
Box(modifier = Modifier.fillMaxWidth().pointerInput(Unit) { Box(modifier = Modifier.fillMaxWidth().pointerInput(Unit) {
detectHorizontalDragGestures( detectHorizontalDragGestures(onDragStart = { velocityTracker.resetTracking() },
onDragStart = { velocityTracker.resetTracking() },
onHorizontalDrag = { change, dragAmount -> onHorizontalDrag = { change, dragAmount ->
velocityTracker.addPosition(change.uptimeMillis, change.position) velocityTracker.addPosition(change.uptimeMillis, change.position)
coroutineScope.launch { offsetX.snapTo(offsetX.value + dragAmount) } 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) if (velocity > 0) rightSwipeCB?.invoke(vm.episode)
else leftSwipeCB?.invoke(vm.episode) else leftSwipeCB?.invoke(vm.episode)
} }
offsetX.animateTo( offsetX.animateTo(targetValue = 0f, animationSpec = tween(500))
targetValue = 0f, // Back to the initial position
animationSpec = tween(500) // Adjust animation duration as needed
)
} }
} }
) )
@ -592,21 +587,21 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
else selected.remove(vms[index].episode) else selected.remove(vms[index].episode)
} }
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
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)) { Row(Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
if (false) { if (false) {
val typedValue = TypedValue() val typedValue = TypedValue()
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true) LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
Icon(painter = painterResource(typedValue.resourceId), tint = textColor, Icon(painter = painterResource(typedValue.resourceId), tint = textColor, contentDescription = "drag handle",
contentDescription = "drag handle",
modifier = Modifier.width(16.dp).align(Alignment.CenterVertically)) modifier = Modifier.width(16.dp).align(Alignment.CenterVertically))
} }
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs() val (imgvCover, checkMark) = createRefs()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode) val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode)
val painter = rememberAsyncImagePainter(model = imgLoc) val painter = rememberAsyncImagePainter(model = imgLoc)
Image( Image(painter = painter, contentDescription = "imgvCover",
painter = painter,
contentDescription = "imgvCover",
modifier = Modifier.width(56.dp).height(56.dp) modifier = Modifier.width(56.dp).height(56.dp)
.constrainAs(imgvCover) { .constrainAs(imgvCover) {
top.linkTo(parent.top) top.linkTo(parent.top)
@ -619,7 +614,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
}) })
) )
val alpha = if (vm.playedState) 1.0f else 0f 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) { modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
end.linkTo(parent.end) end.linkTo(parent.end)
@ -647,31 +644,23 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
if (index >= vms.size) return@LaunchedEffect if (index >= vms.size) return@LaunchedEffect
vms[index].inQueueState = curQueue.contains(vms[index].episode) vms[index].inQueueState = curQueue.contains(vms[index].episode)
} }
val dur = vm.episode.media?.getDuration() ?: 0 Row(verticalAlignment = Alignment.CenterVertically) {
val durText = DurationConverter.getDurationStringLong(dur)
Row {
if (vm.episode.media?.getMediaType() == MediaType.VIDEO) 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 val ratingIconRes = Rating.fromCode(vm.ratingState).res
if (vm.ratingState != Rating.UNRATED.code) 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) 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 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(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium)
} }
Text(vm.episode.title ?: "", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis) 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)
}
}
} }
fun isDownloading(): Boolean { fun isDownloading(): Boolean {
return vms[index].downloadState > DownloadStatus.State.UNKNOWN.ordinal && vms[index].downloadState < DownloadStatus.State.COMPLETED.ordinal return vms[index].downloadState > DownloadStatus.State.UNKNOWN.ordinal && vms[index].downloadState < DownloadStatus.State.COMPLETED.ordinal
@ -697,16 +686,30 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
// vms[index].actionRes = vm.actionButton.getDrawable() // 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 = { detectTapGestures(onLongPress = { vm.showAltActionsDialog = true }, onTap = {
vms[index].actionButton?.onClick(activity) vms[index].actionButton?.onClick(activity)
}) })
}, contentAlignment = Alignment.Center) { }, contentAlignment = Alignment.Center) {
// actionRes = actionButton.getDrawable() // actionRes = actionButton.getDrawable()
Icon(painter = painterResource(vm.actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp)) 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 { try {
val info = StreamInfo.getInfo(Vista.getService(0), url) val info = StreamInfo.getInfo(Vista.getService(0), url)
val episode = episodeFromStreamInfo(info) val episode = episodeFromStreamInfo(info)
addToYoutubeSyndicate(episode, !audioOnly) val status = addToYoutubeSyndicate(episode, !audioOnly)
if (log != null) upsert(log) { it.status = 1 } if (log != null) upsert(log) { it.status = status }
} catch (e: Throwable) { } catch (e: Throwable) {
toastMassege = "Receive share error: ${e.message}" toastMassege = "Receive share error: ${e.message}"
Log.e(TAG, toastMassege) Log.e(TAG, toastMassege)

View File

@ -86,7 +86,7 @@ import java.util.concurrent.Semaphore
private var feedID: Long = 0 private var feedID: Long = 0
private var feed by mutableStateOf<Feed?>(null) private var feed by mutableStateOf<Feed?>(null)
private val episodes = mutableListOf<Episode>() private val episodes = mutableStateListOf<Episode>()
private val vms = mutableStateListOf<EpisodeVM>() private val vms = mutableStateListOf<EpisodeVM>()
private var ieMap: Map<Long, Int> = mapOf() private var ieMap: Map<Long, Int> = mapOf()
@ -159,7 +159,7 @@ import java.util.concurrent.Semaphore
loadItemsRunning = false loadItemsRunning = false
} }
} }
binding.lazyColumn.setContent { binding.mainView.setContent {
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
if (showRemoveFeedDialog) RemoveFeedDialog(listOf(feed!!), onDismissRequest = {showRemoveFeedDialog = false}) { if (showRemoveFeedDialog) RemoveFeedDialog(listOf(feed!!), onDismissRequest = {showRemoveFeedDialog = false}) {
(activity as MainActivity).loadFragment(UserPreferences.defaultPage, null) (activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
@ -248,7 +248,7 @@ import java.util.concurrent.Semaphore
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
start.linkTo(parent.start) start.linkTo(parent.start)
}, verticalAlignment = Alignment.CenterVertically) { }, 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", 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)) modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).combinedClickable(onClick = filterClickCB, onLongClick = filterLongClickCB))
Spacer(modifier = Modifier.width(15.dp)) Spacer(modifier = Modifier.width(15.dp))
@ -259,8 +259,8 @@ import java.util.concurrent.Semaphore
activity.loadChildFragment(fragment, TransitionEffect.SLIDE) activity.loadChildFragment(fragment, TransitionEffect.SLIDE)
} }
})) }))
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(0.5f))
Text(feed?.episodes?.size?.toString()?:"", textAlign = TextAlign.Center, color = Color.White, style = MaterialTheme.typography.bodyLarge) 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", // Image(painter = painterResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner",
// Modifier.width(12.dp).height(12.dp).constrainAs(image1) { // Modifier.width(12.dp).height(12.dp).constrainAs(image1) {

View File

@ -174,7 +174,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
val ratingIconRes = Rating.fromCode(rating).res val ratingIconRes = Rating.fromCode(rating).res
Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", 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 showChooseRatingDialog = true
})) }))
Spacer(modifier = Modifier.weight(0.2f)) Spacer(modifier = Modifier.weight(0.2f))

View File

@ -3,6 +3,7 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.LogsFragmentBinding import ac.mdiq.podcini.databinding.LogsFragmentBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager 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.Feeds.getFeed
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
@ -101,10 +102,10 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable @Composable
fun SharedLogView() { fun SharedLogView() {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val showDialog = remember { mutableStateOf(false) } val showSharedDialog = remember { mutableStateOf(false) }
val dialogParam = remember { mutableStateOf(ShareLog()) } val sharedlogState = remember { mutableStateOf(ShareLog()) }
if (showDialog.value) { if (showSharedDialog.value) {
SharedDetailDialog(status = dialogParam.value, showDialog = showDialog.value, onDismissRequest = { showDialog.value = false }) SharedDetailDialog(status = sharedlogState.value, showDialog = showSharedDialog.value, onDismissRequest = { showSharedDialog.value = false })
} }
var showYTMediaConfirmDialog by remember { mutableStateOf(false) } var showYTMediaConfirmDialog by remember { mutableStateOf(false) }
var sharedUrl by remember { mutableStateOf("") } var sharedUrl by remember { mutableStateOf("") }
@ -116,24 +117,29 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
itemsIndexed(shareLogs) { position, log -> itemsIndexed(shareLogs) { position, log ->
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
Row (modifier = Modifier.clickable { Row (modifier = Modifier.clickable {
if (log.status == 1) { if (log.status < ShareLog.Status.SUCCESS.ordinal) {
showDialog.value = true
dialogParam.value = log
} else {
receiveShared(log.url!!, activity as AppCompatActivity, false) { receiveShared(log.url!!, activity as AppCompatActivity, false) {
sharedUrl = log.url!! sharedUrl = log.url!!
showYTMediaConfirmDialog = true 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 { Column {
Row { Row {
val icon = remember { if (log.status == 1) Icons.Filled.Info else Icons.Filled.Warning } val icon = remember { if (log.status == ShareLog.Status.SUCCESS.ordinal) Icons.Filled.Info else Icons.Filled.Warning }
val iconColor = remember { if (log.status == 1) Color.Green else Color.Yellow } 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)) Icon(icon, "Info", tint = iconColor, modifier = Modifier.padding(end = 2.dp))
Text(formatDateTimeFlex(Date(log.id)), color = textColor) Text(formatDateTimeFlex(Date(log.id)), color = textColor)
Spacer(Modifier.weight(1f)) 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) { if (true || showAction) {
Icon(painter = painterResource(R.drawable.ic_delete), tint = textColor, contentDescription = null, Icon(painter = painterResource(R.drawable.ic_delete), tint = textColor, contentDescription = null,
modifier = Modifier.width(25.dp).height(25.dp).clickable { modifier = Modifier.width(25.dp).height(25.dp).clickable {
@ -141,9 +147,18 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
} }
Text(log.url?:"unknown", color = textColor) 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) 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(log.details, color = Color.Red)
Text(stringResource(R.string.download_error_tap_for_details), color = textColor) Text(stringResource(R.string.download_error_tap_for_details), color = textColor)
} }
@ -355,9 +370,12 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable @Composable
fun SharedDetailDialog(status: ShareLog, showDialog: Boolean, onDismissRequest: () -> Unit) { fun SharedDetailDialog(status: ShareLog, showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) { if (showDialog) {
var message = requireContext().getString(R.string.download_successful) val message = when (status.status) {
if (status.status == 0) message = status.details 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() }) { Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp)) { Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp)) {
Column(modifier = Modifier.padding(10.dp)) { Column(modifier = Modifier.padding(10.dp)) {

View File

@ -33,7 +33,6 @@ import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import android.app.Activity import android.app.Activity
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.app.Dialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
@ -86,8 +85,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.android.material.appbar.MaterialToolbar 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.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -125,7 +122,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var feedCount by mutableStateOf("") private var feedCount by mutableStateOf("")
private var feedSorted by mutableIntStateOf(0) 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 feedListFiltered = mutableStateListOf<Feed>()
private var useGrid by mutableStateOf<Boolean?>(null) private var useGrid by mutableStateOf<Boolean?>(null)
@ -200,7 +197,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onNothingSelected(parent: AdapterView<*>?) {}
} }
feedCount = feedListFiltered.size.toString() + " / " + feedList.size.toString() feedCount = feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString()
loadSubscriptions() loadSubscriptions()
return binding.root return binding.root
} }
@ -224,7 +221,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onDestroyView() { override fun onDestroyView() {
Logd(TAG, "onDestroyView") Logd(TAG, "onDestroyView")
feedList = mutableListOf() // feedList = mutableListOf()
feedListFiltered.clear() feedListFiltered.clear()
_binding = null _binding = null
super.onDestroyView() super.onDestroyView()
@ -345,10 +342,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (!loadItemsRunning) { if (!loadItemsRunning) {
loadItemsRunning = true loadItemsRunning = true
lifecycleScope.launch { lifecycleScope.launch {
val feedList: List<Feed>
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
resetTags() resetTags()
filterAndSort() feedList = filterAndSort()
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// We have fewer items. This can result in items being selected that are no longer visible. // 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() // filterOnTag()
feedListFiltered.clear() feedListFiltered.clear()
feedListFiltered.addAll(feedList) feedListFiltered.addAll(feedList)
feedCount = feedListFiltered.size.toString() + " / " + feedList.size.toString() feedCount = feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString()
infoTextFiltered = " " infoTextFiltered = " "
if (feedsFilter.isNotEmpty()) infoTextFiltered = getString(R.string.filtered_label) if (feedsFilter.isNotEmpty()) infoTextFiltered = getString(R.string.filtered_label)
txtvInformation = (infoTextFiltered + infoTextUpdate) 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() var fQueryStr = FeedFilter(feedsFilter).queryString()
val tagsQueryStr = queryStringOfTags() val tagsQueryStr = queryStringOfTags()
if (tagsQueryStr.isNotEmpty()) fQueryStr += " AND $tagsQueryStr" if (tagsQueryStr.isNotEmpty()) fQueryStr += " AND $tagsQueryStr"
@ -478,8 +476,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
comparator(counterMap, dir) comparator(counterMap, dir)
} }
} }
synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() } // synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() }
feedSorted++ feedSorted++
return feedList_.sortedWith(comparator)
} }
private fun comparator(counterMap: Map<Long, Long>, dir: Int): Comparator<Feed> { private fun comparator(counterMap: Map<Long, Long>, dir: Int): Comparator<Feed> {

View File

@ -28,7 +28,7 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.compose.ui.platform.ComposeView <androidx.compose.ui.platform.ComposeView
android:id="@+id/lazyColumn" android:id="@+id/mainView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>

View File

@ -3,6 +3,12 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> 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 <item
android:id="@+id/show_shared_logs" android:id="@+id/show_shared_logs"
android:title="@string/show_shared_logs_label" android:title="@string/show_shared_logs_label"
@ -15,12 +21,6 @@
android:icon="@drawable/ic_subscriptions" android:icon="@drawable/ic_subscriptions"
app:showAsAction="always" /> 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 <item
android:id="@+id/clear_logs_item" android:id="@+id/clear_logs_item"
android:title="@string/clear_history_label" android:title="@string/clear_history_label"

View File

@ -67,6 +67,8 @@
<string name="tts_not_available">TTS engine not available</string> <string name="tts_not_available">TTS engine not available</string>
<string name="episode_has_no_content">Episode has not content</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_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_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> <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="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="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 --> <!-- Download messages and labels -->
<string name="download_successful">successful</string> <string name="download_successful">successful</string>
<string name="download_pending">Download pending</string> <string name="download_pending">Download pending</string>

View File

@ -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 # 6.11.4
* corrected color contrast in SwipeActions dialog * corrected color contrast in SwipeActions dialog

View File

@ -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