From ca6afe3c27a8a1de282e6f8e76f840414da266b0 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:47:44 +0100 Subject: [PATCH] 6.11.5 commit --- README.md | 2 +- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 1 + .../ac/mdiq/podcini/storage/database/Feeds.kt | 5 +- .../ac/mdiq/podcini/storage/model/ShareLog.kt | 8 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 84 ++++++- .../ac/mdiq/podcini/ui/compose/EpisodesVM.kt | 229 +++++++++--------- .../ui/fragment/FeedEpisodesFragment.kt | 10 +- .../podcini/ui/fragment/FeedInfoFragment.kt | 2 +- .../mdiq/podcini/ui/fragment/LogsFragment.kt | 52 ++-- .../ui/fragment/SubscriptionsFragment.kt | 19 +- .../res/layout/feed_item_list_fragment.xml | 2 +- app/src/main/res/menu/logs.xml | 12 +- app/src/main/res/values/strings.xml | 4 + changelog.md | 12 + .../android/en-US/changelogs/3020275.txt | 11 + 16 files changed, 292 insertions(+), 167 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/3020275.txt diff --git a/README.md b/README.md index 489dd8ba..5f535bbd 100644 --- a/README.md +++ b/README.md @@ -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]() as of Feb 5 2024. diff --git a/app/build.gradle b/app/build.gradle index 1c8b4ec6..85d09db4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 = "" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 38ae72ec..8e52ff4d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ + +// 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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt index c3b9e979..535af167 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt @@ -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, 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, 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, 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, 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, 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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index 461e7798..a107ff4c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -86,7 +86,7 @@ import java.util.concurrent.Semaphore private var feedID: Long = 0 private var feed by mutableStateOf(null) - private val episodes = mutableListOf() + private val episodes = mutableStateListOf() private val vms = mutableStateListOf() private var ieMap: Map = 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) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index 342fe9ae..751a6668 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -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)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt index 6d0fba0b..1ed40388 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt @@ -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)) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index 2bce6284..74539529 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -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 = mutableListOf() +// private var feedList: MutableList = mutableListOf() private var feedListFiltered = mutableStateListOf() private var useGrid by mutableStateOf(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 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 { 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, dir: Int): Comparator { diff --git a/app/src/main/res/layout/feed_item_list_fragment.xml b/app/src/main/res/layout/feed_item_list_fragment.xml index 9b880975..4b5533f4 100644 --- a/app/src/main/res/layout/feed_item_list_fragment.xml +++ b/app/src/main/res/layout/feed_item_list_fragment.xml @@ -28,7 +28,7 @@ diff --git a/app/src/main/res/menu/logs.xml b/app/src/main/res/menu/logs.xml index fc8b3ae3..fa016d45 100644 --- a/app/src/main/res/menu/logs.xml +++ b/app/src/main/res/menu/logs.xml @@ -3,6 +3,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> + + - - TTS engine not available Episode has not content + 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. + Since Android 13, top level notification is needed for normal refresh and playback. You may disallow notifications of sub-catergories at your wish. You denied the permission. If you disable notifications and something goes wrong, you might be unable to find out why it went wrong. @@ -312,6 +314,8 @@ No items selected Warning: you are deleting local episodes. It will delete the media files from your device storage. It cannot be downloaded again through Podcini. Continue? + existing + successful Download pending diff --git a/changelog.md b/changelog.md index b1b2a6b0..fc2c50e1 100644 --- a/changelog.md +++ b/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 diff --git a/fastlane/metadata/android/en-US/changelogs/3020275.txt b/fastlane/metadata/android/en-US/changelogs/3020275.txt new file mode 100644 index 00000000..257ecc73 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020275.txt @@ -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