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)
#### 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.

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}
// 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)

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.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,121 +587,129 @@ 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)) {
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",
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",
modifier = Modifier.width(56.dp).height(56.dp)
.constrainAs(imgvCover) {
top.linkTo(parent.top)
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",
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",
modifier = Modifier.width(56.dp).height(56.dp)
.constrainAs(imgvCover) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}.clickable(onClick = {
Logd(TAG, "icon clicked!")
if (selectMode) toggleSelected()
else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
})
)
val alpha = if (vm.playedState) 1.0f else 0f
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)
start.linkTo(parent.start)
}.clickable(onClick = {
Logd(TAG, "icon clicked!")
if (selectMode) toggleSelected()
else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
end.linkTo(parent.end)
})
)
val alpha = if (vm.playedState) 1.0f else 0f
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)
})
}
Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp)
.combinedClickable(onClick = {
Logd(TAG, "clicked: ${vm.episode.title}")
if (selectMode) toggleSelected()
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
}, onLongClick = {
selectMode = !selectMode
vm.isSelected = selectMode
selected.clear()
if (selectMode) {
selected.add(vms[index].episode)
longPressIndex = index
} else {
selectedSize = 0
longPressIndex = -1
}
Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp)
.combinedClickable(onClick = {
Logd(TAG, "clicked: ${vm.episode.title}")
if (selectMode) toggleSelected()
else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
}, onLongClick = {
selectMode = !selectMode
vm.isSelected = selectMode
selected.clear()
if (selectMode) {
selected.add(vms[index].episode)
longPressIndex = index
} else {
selectedSize = 0
longPressIndex = -1
}
Logd(TAG, "long clicked: ${vm.episode.title}")
})) {
LaunchedEffect(key1 = queueChanged) {
if (index >= vms.size) return@LaunchedEffect
vms[index].inQueueState = curQueue.contains(vms[index].episode)
}
Logd(TAG, "long clicked: ${vm.episode.title}")
})) {
LaunchedEffect(key1 = queueChanged) {
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 {
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))
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))
if (vm.inQueueState)
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 ""
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)
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))
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))
if (vm.inQueueState)
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 ""
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium)
}
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
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
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()}")
vm.actionRes = vm.actionButton!!.getDrawable()
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
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
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()}")
vm.actionRes = vm.actionButton!!.getDrawable()
}
// LaunchedEffect(vm.isPlayingState) {
// Logd(TAG, "LaunchedEffect isPlayingState: $index ${vms[index].isPlayingState} ${vm.isPlayingState}")
// vms[index].actionButton = EpisodeActionButton.forItem(vms[index].episode)
// vms[index].actionRes = vm.actionButton.getDrawable()
// }
}
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) {
}
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))
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 (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)

View File

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

View File

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

View File

@ -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 {"" }
Text(statusText, color = textColor)
if (log.status != 1) {
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)
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)) {

View File

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

View File

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

View File

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

View File

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

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