diff --git a/README.md b/README.md
index b59f04db..e5710e31 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
[](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13)
[](https://www.openapk.net/podcini/ac.mdiq.podcini/)
+#### Podcini.R 6.10 allows creating synthetic podcast and shelving any episdes to any synthetic podcasts
#### Podcini.R version 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Since 6.6, podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs
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.
@@ -29,10 +30,11 @@ Compared to AntennaPod this project:
4. Boasts new UI's including streamlined drawer, subscriptions view and player controller,
5. Supports multiple, virtual and circular play queues associable to any podcast
6. Auto-download is governed by policy and limit settings of individual feed
-7. Features synthetic podcasts while supporting channels, playlists, single media from YouTube and YT Music, as well as normal podcasts and plain RSS,
-8. Allows adding personal notes and 5-level rating on every episode
-9. Offers Readability and Text-to-Speech for RSS contents,s
-10. Features `instant sync` across devices without a server.
+7. Features synthetic podcasts and allows episodes to be shelved to any synthetic podcast
+8. Supports channels, playlists, single media from YouTube and YT Music, as well as normal podcasts and plain RSS,
+9. Allows adding personal notes and 5-level rating on every episode
+10. Offers Readability and Text-to-Speech for RSS contents,s
+11. Features `instant sync` across devices without a server.
The project aims to profit from modern frameworks, improve efficiency and provide more useful and user-friendly features.
diff --git a/app/build.gradle b/app/build.gradle
index a3434723..83b1403a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- versionCode 3020268
- versionName "6.9.3"
+ versionCode 3020269
+ versionName "6.10.0"
applicationId "ac.mdiq.podcini.R"
def commit = ""
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
index a83c8219..8843dea3 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
@@ -39,6 +39,7 @@ import androidx.annotation.OptIn
import androidx.core.app.NotificationManagerCompat
import androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi
+import io.realm.kotlin.ext.isManaged
import kotlinx.coroutines.Job
import java.io.File
import java.util.*
@@ -247,14 +248,14 @@ object Episodes {
}
}
- @JvmStatic
- fun setFavorite(episode: Episode, stat: Boolean?) : Job {
- Logd(TAG, "setFavorite called $stat")
- return runOnIOScope {
- val result = upsert(episode) { it.rating = if (stat ?: !it.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code }
- EventFlow.postEvent(FlowEvent.RatingEvent(result, result.rating))
- }
- }
+// @JvmStatic
+// fun setFavorite(episode: Episode, stat: Boolean?) : Job {
+// Logd(TAG, "setFavorite called $stat")
+// return runOnIOScope {
+// val result = upsert(episode) { it.rating = if (stat ?: !it.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code }
+// EventFlow.postEvent(FlowEvent.RatingEvent(result, result.rating))
+// }
+// }
fun setRating(episode: Episode, rating: Int) : Job {
Logd(TAG, "setRating called $rating")
@@ -283,7 +284,9 @@ object Episodes {
@OptIn(UnstableApi::class)
suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode {
Logd(TAG, "setPlayStateSync called played: $played resetMediaPosition: $resetMediaPosition ${episode.title}")
- val result = upsert(episode) {
+ var episode_ = episode
+ if (!episode.isManaged()) episode_ = realm.query(Episode::class).query("id == $0", episode.id).first().find() ?: episode
+ val result = upsert(episode_) {
if (played >= PlayState.NEW.code && played <= PlayState.BUILDING.code) it.playState = played
else {
if (it.playState == PlayState.PLAYED.code) it.playState = PlayState.UNPLAYED.code
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
index 74c0eb29..a0fb8874 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
@@ -423,17 +423,11 @@ object Feeds {
var feed = getFeed(feedId, true)
if (feed != null) return feed
- feed = Feed()
- feed.id = feedId
- if (music) feed.title = "YTMusic Syndicate" + if (video) "" else " Audio"
- else feed.title = "Youtube Syndicate" + if (video) "" else " Audio"
+ val name = if (music) "YTMusic Syndicate" + if (video) "" else " Audio"
+ else "Youtube Syndicate" + if (video) "" else " Audio"
+ feed = createSynthetic(feedId, name)
feed.type = Feed.FeedType.YOUTUBE.name
feed.hasVideoMedia = video
- feed.downloadUrl = null
- feed.fileUrl = File(feedfilePath, getFeedfileName(feed)).toString()
- feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
- feed.preferences!!.keepUpdated = false
- feed.preferences!!.queueId = -2L
feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
upsertBlk(feed) {}
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
@@ -456,21 +450,35 @@ object Feeds {
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
}
- private fun getMiscSyndicate(): Feed {
- var feedId: Long = 11
- var feed = getFeed(feedId, true)
- if (feed != null) return feed
-
- feed = Feed()
- feed.id = feedId
- feed.title = "Misc Syndicate"
- feed.type = Feed.FeedType.RSS.name
+ fun createSynthetic(feedId: Long, name: String): Feed {
+ val feed = Feed()
+ var feedId_ = feedId
+ if (feedId_ <= 0) {
+ var i = 100L
+ while (true) {
+ if (getFeed(i++) != null) continue
+ feedId_ = --i
+ break
+ }
+ }
+ feed.id = feedId_
+ feed.title = name
+ feed.author = "Yours Truly"
feed.downloadUrl = null
feed.fileUrl = File(feedfilePath, getFeedfileName(feed)).toString()
feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
feed.preferences!!.keepUpdated = false
feed.preferences!!.queueId = -2L
-// feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
+ return feed
+ }
+
+ private fun getMiscSyndicate(): Feed {
+ val feedId: Long = 11
+ var feed = getFeed(feedId, true)
+ if (feed != null) return feed
+
+ feed = createSynthetic(feedId, "Misc Syndicate")
+ feed.type = Feed.FeedType.RSS.name
upsertBlk(feed) {}
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
return feed
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMenuHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMenuHandler.kt
deleted file mode 100644
index b781df0d..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMenuHandler.kt
+++ /dev/null
@@ -1,196 +0,0 @@
-package ac.mdiq.podcini.ui.actions
-
-import ac.mdiq.podcini.R
-import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
-import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey
-import ac.mdiq.podcini.net.sync.model.EpisodeAction
-import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
-import ac.mdiq.podcini.playback.base.InTheatre
-import ac.mdiq.podcini.playback.base.InTheatre.curQueue
-import ac.mdiq.podcini.playback.base.InTheatre.curState
-import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
-import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE
-import ac.mdiq.podcini.receiver.MediaButtonReceiver
-import ac.mdiq.podcini.storage.database.Episodes.setFavorite
-import ac.mdiq.podcini.storage.database.Episodes.setPlayState
-import ac.mdiq.podcini.storage.database.Queues.addToQueue
-import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
-import ac.mdiq.podcini.storage.model.Episode
-import ac.mdiq.podcini.storage.model.EpisodeMedia
-import ac.mdiq.podcini.ui.dialog.ShareDialog
-import ac.mdiq.podcini.ui.utils.LocalDeleteModal
-import ac.mdiq.podcini.util.IntentUtils
-import ac.mdiq.podcini.util.Logd
-import ac.mdiq.podcini.util.ShareUtils
-import android.view.KeyEvent
-import android.view.Menu
-import androidx.annotation.OptIn
-import androidx.fragment.app.Fragment
-import androidx.media3.common.util.UnstableApi
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-
-/**
- * Handles interactions with the FeedItemMenu.
- */
-@OptIn(UnstableApi::class)
-object EpisodeMenuHandler {
- private val TAG: String = EpisodeMenuHandler::class.simpleName ?: "Anonymous"
-
- /**
- * This method should be called in the prepare-methods of menus. It changes
- * the visibility of the menu items depending on a FeedItem's attributes.
- * @param menu An instance of Menu
- * @param selectedItem The FeedItem for which the menu is supposed to be prepared
- * @return Returns true if selectedItem is not null.
- */
- @UnstableApi
- fun onPrepareMenu(menu: Menu?, selectedItem: Episode?): Boolean {
- if (menu == null || selectedItem == null) return false
-
- val hasMedia = selectedItem.media != null
- val isPlaying = hasMedia && InTheatre.isCurMedia(selectedItem.media)
- val isInQueue: Boolean = curQueue.contains(selectedItem)
- val isLocalFile = hasMedia && selectedItem.feed?.isLocalFeed?:false
- val isFavorite: Boolean = selectedItem.isFavorite
-
- setItemVisibility(menu, R.id.skip_episode_item, isPlaying)
- setItemVisibility(menu, R.id.remove_from_queue_item, isInQueue)
- setItemVisibility(menu, R.id.add_to_queue_item, !isInQueue && selectedItem.media != null)
- setItemVisibility(menu, R.id.visit_website_item, !(selectedItem.feed?.isLocalFeed?:false) && ShareUtils.hasLinkToShare(selectedItem))
- setItemVisibility(menu, R.id.share_item, !(selectedItem.feed?.isLocalFeed?:false))
- setItemVisibility(menu, R.id.mark_read_item, !selectedItem.isPlayed())
- setItemVisibility(menu, R.id.mark_unread_item, selectedItem.isPlayed())
- setItemVisibility(menu, R.id.reset_position, hasMedia && selectedItem.media?.getPosition() != 0)
-
- // Display proper strings when item has no media
- if (hasMedia) {
- setItemTitle(menu, R.id.mark_read_item, R.string.mark_read_label)
- setItemTitle(menu, R.id.mark_unread_item, R.string.mark_unread_label)
- } else {
- setItemTitle(menu, R.id.mark_read_item, R.string.mark_read_no_media_label)
- setItemTitle(menu, R.id.mark_unread_item, R.string.mark_unread_label_no_media)
- }
-
-// setItemVisibility(menu, R.id.add_to_favorites_item, !isFavorite)
-// setItemVisibility(menu, R.id.remove_from_favorites_item, isFavorite)
-
- CoroutineScope(Dispatchers.Main).launch {
- val fileDownloaded = withContext(Dispatchers.IO) { hasMedia && selectedItem.media?.fileExists() ?: false }
- setItemVisibility(menu, R.id.remove_item, fileDownloaded || isLocalFile)
- }
- return true
- }
-
- /**
- * Used to set the viability of a menu item.
- * This method also does some null-checking so that neither menu nor the menu item are null
- * in order to prevent nullpointer exceptions.
- * @param menu The menu that should be used
- * @param menuId The id of the menu item that will be used
- * @param visibility The new visibility status of given menu item
- */
- private fun setItemVisibility(menu: Menu?, menuId: Int, visibility: Boolean) {
- if (menu == null) return
- val item = menu.findItem(menuId)
- item?.setVisible(visibility)
- }
-
- /**
- * This method allows to replace to String of a menu item with a different one.
- * @param menu Menu item that should be used
- * @param id The id of the string that is going to be replaced.
- * @param noMedia The id of the new String that is going to be used.
- */
- private fun setItemTitle(menu: Menu, id: Int, noMedia: Int) {
- val item = menu.findItem(id)
- item?.setTitle(noMedia)
- }
-
- /**
- * The same method as [.onPrepareMenu], but lets the
- * caller also specify a list of menu items that should not be shown.
- * @param excludeIds Menu item that should be excluded
- * @return true if selectedItem is not null.
- */
- @UnstableApi
- fun onPrepareMenu(menu: Menu?, selectedItem: Episode?, vararg excludeIds: Int): Boolean {
- if (menu == null || selectedItem == null) return false
- val rc = onPrepareMenu(menu, selectedItem)
- if (rc && excludeIds.isNotEmpty()) {
- for (id in excludeIds) setItemVisibility(menu, id, false)
- }
- return rc
- }
-
- /**
- * Default menu handling for the given FeedItem.
- * A Fragment instance, (rather than the more generic Context), is needed as a parameter
- * to support some UI operations, e.g., creating a Snackbar.
- */
- fun onMenuItemClicked(fragment: Fragment, menuItemId: Int, selectedItem: Episode): Boolean {
- val context = fragment.requireContext()
- when (menuItemId) {
- R.id.skip_episode_item -> context.sendBroadcast(MediaButtonReceiver.createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT))
- R.id.remove_item -> {
- LocalDeleteModal.deleteEpisodesWarnLocal(context, listOf(selectedItem))
- }
- R.id.mark_read_item -> {
-// selectedItem.setPlayed(true)
- setPlayState(Episode.PlayState.PLAYED.code, true, selectedItem)
- if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) {
- val media: EpisodeMedia? = selectedItem.media
- // not all items have media, Gpodder only cares about those that do
- if (isProviderConnected && media != null) {
- val actionPlay: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY)
- .currentTimestamp()
- .started(media.getDuration() / 1000)
- .position(media.getDuration() / 1000)
- .total(media.getDuration() / 1000)
- .build()
- SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, actionPlay)
- }
- }
- }
- R.id.mark_unread_item -> {
-// selectedItem.setPlayed(false)
- setPlayState(Episode.PlayState.UNPLAYED.code, false, selectedItem)
- if (isProviderConnected && selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) {
- val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
- .currentTimestamp()
- .build()
- SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, actionNew)
- }
- }
- R.id.add_to_queue_item -> addToQueue(true, selectedItem)
- R.id.remove_from_queue_item -> removeFromQueue(selectedItem)
-// R.id.add_to_favorites_item -> setFavorite(selectedItem, true)
-// R.id.remove_from_favorites_item -> setFavorite(selectedItem, false)
- R.id.reset_position -> {
- selectedItem.media?.setPosition(0)
- if (curState.curMediaId == (selectedItem.media?.id ?: "")) {
- writeNoMediaPlaying()
- IntentUtils.sendLocalBroadcast(context, ACTION_SHUTDOWN_PLAYBACK_SERVICE)
- }
- setPlayState(Episode.PlayState.UNPLAYED.code, true, selectedItem)
- }
- R.id.visit_website_item -> {
- val url = selectedItem.getLinkWithFallback()
- if (url != null) IntentUtils.openInBrowser(context, url)
- }
- R.id.share_item -> {
- val shareDialog: ShareDialog = ShareDialog.newInstance(selectedItem)
- shareDialog.show((fragment.requireActivity().supportFragmentManager), "ShareEpisodeDialog")
- }
- else -> {
- Logd(TAG, "Unknown menuItemId: $menuItemId")
- return false
- }
- }
- // Refresh menu state
- return true
- }
-}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/MenuItemUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/MenuItemUtils.kt
index 983455e3..9456d45c 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/MenuItemUtils.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/MenuItemUtils.kt
@@ -13,7 +13,6 @@ object MenuItemUtils {
* context menu was created from. This assigns the listener to every menu item,
* so that the correct fragment is always called first and can consume the click.
*
- *
* Note that Android still calls the onContextItemSelected methods of all fragments
* when the passed listener returns false.
*/
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
index 2a1a123e..27720532 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt
@@ -59,7 +59,7 @@ class ShareReceiverActivity : AppCompatActivity() {
CustomTheme(this) {
confirmAddYoutubeEpisode(listOf(sharedUrl!!), showDialog.value, onDismissRequest = {
showDialog.value = false
-// finish()
+ finish()
})
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt
index f09040c7..d7d029b6 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt
@@ -25,7 +25,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
-import ac.mdiq.podcini.storage.database.Episodes.setFavorite
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
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 ff7c4d5b..7d1d19c3 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
@@ -17,10 +17,9 @@ import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsert
-import ac.mdiq.podcini.storage.model.Episode
-import ac.mdiq.podcini.storage.model.MediaType
-import ac.mdiq.podcini.storage.model.PlayQueue
-import ac.mdiq.podcini.storage.model.ShareLog
+import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
+import ac.mdiq.podcini.storage.model.*
+import ac.mdiq.podcini.storage.model.Feed.Companion.newId
import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
@@ -248,6 +247,62 @@ fun PutToQueueDialog(selected: List, onDismissRequest: () -> Unit) {
}
}
+@Composable
+fun ShelveDialog(selected: List, onDismissRequest: () -> Unit) {
+ val synthetics = realm.query(Feed::class).query("id >= 100 && id <= 1000").find()
+ Dialog(onDismissRequest = onDismissRequest) {
+ Surface(shape = RoundedCornerShape(16.dp)) {
+ val scrollState = rememberScrollState()
+ Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
+ var removeChecked by remember { mutableStateOf(false) }
+ var toFeed by remember { mutableStateOf(null) }
+ for (f in synthetics) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ RadioButton(selected = toFeed == f, onClick = { toFeed = f })
+ Text(f.title ?: "No title",)
+ }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Checkbox(checked = removeChecked, onCheckedChange = { removeChecked = it })
+ Text(text = stringResource(R.string.remove_from_current_feed), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp))
+ }
+ if (toFeed != null) Row {
+ Spacer(Modifier.weight(1f))
+ Button(onClick = {
+ val eList: MutableList = mutableListOf()
+ for (e in selected) {
+ var e_ = e
+ if (!removeChecked || (e.feedId != null && e.feedId!! >= 1000L)) {
+ e_ = realm.copyFromRealm(e)
+ e_.id = newId()
+ e_.media?.id = e_.id
+ } else {
+ val feed = realm.query(Feed::class).query("id == $0", e_.feedId).first().find()
+ if (feed != null) {
+ upsertBlk(feed) {
+ it.episodes.remove(e_)
+ }
+ }
+ }
+ upsertBlk(e_) {
+ it.feed = toFeed
+ it.feedId = toFeed!!.id
+ eList.add(it)
+ }
+ }
+ upsertBlk(toFeed!!) {
+ it.episodes.addAll(eList)
+ }
+ onDismissRequest()
+ }) {
+ Text("Confirm")
+ }
+ }
+ }
+ }
+ }
+}
+
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, refreshCB: (()->Unit)? = null,
@@ -273,6 +328,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
var showPutToQueueDialog by remember { mutableStateOf(false) }
if (showPutToQueueDialog) PutToQueueDialog(selected) { showPutToQueueDialog = false }
+ var showShelveDialog by remember { mutableStateOf(false) }
+ if (showShelveDialog) ShelveDialog(selected) { showShelveDialog = false }
+
@Composable
fun EpisodeSpeedDial(modifier: Modifier = Modifier) {
var isExpanded by remember { mutableStateOf(false) }
@@ -284,7 +342,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
Logd(TAG, "ic_delete: ${selected.size}")
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "")
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "Delete media")
Text(stringResource(id = R.string.delete_episode_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable {
@@ -296,7 +354,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
?.download(activity, episode)
}
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "")
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "Download")
Text(stringResource(id = R.string.download_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable {
@@ -305,7 +363,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
Logd(TAG, "ic_mark_played: ${selected.size}")
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "")
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "Toggle played state")
Text(stringResource(id = R.string.toggle_played_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable {
@@ -314,7 +372,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
Logd(TAG, "ic_playlist_remove: ${selected.size}")
removeFromQueue(*selected.toTypedArray())
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "")
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "Remove from active queue")
Text(stringResource(id = R.string.remove_from_queue_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable {
@@ -323,17 +381,25 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
Logd(TAG, "ic_playlist_play: ${selected.size}")
Queues.addToQueue(true, *selected.toTypedArray())
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "Add to active queue")
Text(stringResource(id = R.string.add_to_queue_label)) } },
+ { Row(modifier = Modifier.padding(horizontal = 16.dp)
+ .clickable {
+ isExpanded = false
+ selectMode = false
+ Logd(TAG, "shelve_label: ${selected.size}")
+ showShelveDialog = true
+ }, verticalAlignment = Alignment.CenterVertically) {
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.baseline_shelves_24), "Shelve")
+ Text(stringResource(id = R.string.shelve_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable {
isExpanded = false
selectMode = false
Logd(TAG, "ic_playlist_play: ${selected.size}")
showPutToQueueDialog = true
-// PutToQueueDialog(activity, selected).show()
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "Add to queue...")
Text(stringResource(id = R.string.put_in_queue_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable {
@@ -342,7 +408,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
showChooseRatingDialog = true
isExpanded = false
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "")
+ Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "Set rating")
Text(stringResource(id = R.string.set_rating_label)) } },
)
if (selected.isNotEmpty() && selected[0].isRemote.value)
@@ -367,7 +433,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
}
}
}, verticalAlignment = Alignment.CenterVertically) {
- Icon(Icons.Filled.AddCircle, "")
+ Icon(Icons.Filled.AddCircle, "Reserve episodes")
Text(stringResource(id = R.string.reserve_episodes_label))
}
}
@@ -458,8 +524,10 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList,
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode)
+ Logd(TAG, "imgLoc: $imgLoc")
AsyncImage(model = imgLoc, contentDescription = "imgvCover",
placeholder = painterResource(R.mipmap.ic_launcher),
+ error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(56.dp).height(56.dp)
.constrainAs(imgvCover) {
top.linkTo(parent.top)
@@ -614,39 +682,38 @@ fun confirmAddYoutubeEpisode(sharedUrls: List, showDialog: Boolean, onDi
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
- Card(
- modifier = Modifier
- .wrapContentSize(align = Alignment.Center)
- .padding(16.dp),
- shape = RoundedCornerShape(16.dp),
- ) {
+ Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
var audioOnly by remember { mutableStateOf(false) }
Row(Modifier.fillMaxWidth()) {
Checkbox(checked = audioOnly, onCheckedChange = { audioOnly = it })
Text(text = stringResource(R.string.pref_video_mode_audio_only), style = MaterialTheme.typography.bodyLarge.merge())
}
- Button(onClick = {
- CoroutineScope(Dispatchers.IO).launch {
- for (url in sharedUrls) {
- val log = realm.query(ShareLog::class).query("url == $0", url).first().find()
- try {
- val info = StreamInfo.getInfo(Vista.getService(0), url)
- val episode = episodeFromStreamInfo(info)
- addToYoutubeSyndicate(episode, !audioOnly)
- if (log != null) upsert(log) { it.status = 1 }
- } catch (e: Throwable) {
- toastMassege = "Receive share error: ${e.message}"
- Log.e(TAG, toastMassege)
- if (log != null) upsert(log) { it.details = e.message?: "error" }
- withContext(Dispatchers.Main) { showToast = true }
+ var showComfirmButton by remember { mutableStateOf(true) }
+ if (showComfirmButton) {
+ Button(onClick = {
+ showComfirmButton = false
+ CoroutineScope(Dispatchers.IO).launch {
+ for (url in sharedUrls) {
+ val log = realm.query(ShareLog::class).query("url == $0", url).first().find()
+ try {
+ val info = StreamInfo.getInfo(Vista.getService(0), url)
+ val episode = episodeFromStreamInfo(info)
+ addToYoutubeSyndicate(episode, !audioOnly)
+ if (log != null) upsert(log) { it.status = 1 }
+ } catch (e: Throwable) {
+ toastMassege = "Receive share error: ${e.message}"
+ Log.e(TAG, toastMassege)
+ if (log != null) upsert(log) { it.details = e.message?: "error" }
+ withContext(Dispatchers.Main) { showToast = true }
+ }
}
+ withContext(Dispatchers.Main) { onDismissRequest() }
}
+ }) {
+ Text("Confirm")
}
- onDismissRequest()
- }) {
- Text("Confirm")
- }
+ } else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
}
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/OnlineFeed.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/OnlineFeed.kt
index 44362e5a..9bfcbcea 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/OnlineFeed.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/OnlineFeed.kt
@@ -82,9 +82,8 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult) {
Row {
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs()
- AsyncImage(model = feed.imageUrl,
- contentDescription = "imgvCover",
- placeholder = painterResource(R.mipmap.ic_launcher),
+ AsyncImage(model = feed.imageUrl, contentDescription = "imgvCover",
+ placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(65.dp).height(65.dp).constrainAs(imgvCover) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt
index 098032d6..3a33a761 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt
@@ -30,9 +30,10 @@ class CustomFeedNameDialog(activity: Activity, private var feed: Feed) {
.setTitle(R.string.rename_feed_label)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val newTitle = binding.editText.text.toString()
- feed = unmanaged(feed)
- feed.setCustomTitle1(newTitle)
- feed = upsertBlk(feed) {}
+// feed = unmanaged(feed)
+ feed = upsertBlk(feed) {
+ it.setCustomTitle1(newTitle)
+ }
}
.setNeutralButton(R.string.reset, null)
.setNegativeButton(R.string.cancel_label, null)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt
index 4b500305..fd1d4abc 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt
@@ -32,16 +32,14 @@ import ac.mdiq.podcini.storage.utils.ChapterUtils
import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
-import ac.mdiq.podcini.ui.actions.EpisodeMenuHandler
+//import ac.mdiq.podcini.ui.actions.EpisodeMenuHandler
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme
-import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog
-import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
-import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
-import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
+import ac.mdiq.podcini.ui.dialog.*
+import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment.EpisodeHomeFragment.Companion.episode
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow
@@ -231,25 +229,26 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (curMedia == null) return
if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start()
}
- AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher),
+ AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(70.dp).height(70.dp).padding(start = 5.dp)
.clickable(onClick = {
- Logd(TAG, "icon clicked!")
- Logd(TAG, "playerUiFragment was clicked")
- val media = curMedia
- if (media != null) {
- val mediaType = media.getMediaType()
- if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY
- || (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) {
- Logd(TAG, "popping as audio episode")
- ensureService()
- (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
- } else {
- Logd(TAG, "popping video activity")
- val intent = getPlayerActivityIntent(requireContext(), mediaType)
- startActivity(intent)
+ Logd(TAG, "playerUiFragment icon was clicked")
+ if (isCollapsed) {
+ val media = curMedia
+ if (media != null) {
+ val mediaType = media.getMediaType()
+ if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY
+ || (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) {
+ Logd(TAG, "popping as audio episode")
+ ensureService()
+ (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
+ } else {
+ Logd(TAG, "popping video activity")
+ val intent = getPlayerActivityIntent(requireContext(), mediaType)
+ startActivity(intent)
+ }
}
- }
+ } else (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
}))
Spacer(Modifier.weight(0.1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -414,7 +413,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
modifier = Modifier.width(36.dp).height(36.dp).padding(end = 10.dp).clickable(onClick = { seekToNextChapter() }))
}
}
- AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher),
+ AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.fillMaxWidth().padding(start = 32.dp, end = 32.dp, top = 10.dp).clickable(onClick = {
}))
}
@@ -730,11 +729,9 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private fun setChapterDividers() {
if (currentMedia == null) return
-
if (currentMedia!!.getChapters().isNotEmpty()) {
val chapters: List = currentMedia!!.getChapters()
val dividerPos = FloatArray(chapters.size)
-
for (i in chapters.indices) {
dividerPos[i] = chapters[i].start / curDurationFB.toFloat()
}
@@ -929,7 +926,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private fun onRatingEvent(event: FlowEvent.RatingEvent) {
if (curEpisode?.id == event.episode.id) {
rating = event.rating
- EpisodeMenuHandler.onPrepareMenu(toolbar.menu, event.episode)
+// EpisodeMenuHandler.onPrepareMenu(toolbar.menu, event.episode)
}
}
@@ -939,7 +936,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val isEpisodeMedia = currentMedia is EpisodeMedia
toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia)
val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episodeOrFetch() else null
- EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
+// EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
val mediaType = curMedia?.getMediaType()
val notAudioOnly = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY
@@ -955,7 +952,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
val media: Playable = curMedia ?: return false
val feedItem = if (media is EpisodeMedia) media.episodeOrFetch() else null
- if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true
+// if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true
val itemId = menuItem.itemId
when (itemId) {
@@ -988,6 +985,12 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
context.startActivity(intent)
}
}
+ R.id.share_item -> {
+ if (currentItem != null) {
+ val shareDialog: ShareDialog = ShareDialog.newInstance(currentItem!!)
+ shareDialog.show((requireActivity().supportFragmentManager), "ShareEpisodeDialog")
+ }
+ }
else -> return false
}
return true
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt
index 50cc12c1..797ebb4a 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt
@@ -176,7 +176,7 @@ import java.util.*
val items = realm.query(Episode::class).query("media.episode == nil").find()
Logd(TAG, "number of episode with null backlink: ${items.size}")
for (item in items) {
- upsert(item) { it.media!!.episode = it }
+ if (item.media != null ) upsert(item) { it.media!!.episode = it }
}
nameEpisodeMap.clear()
for (e in episodes) {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
index 2f4e7510..5b244cf2 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
@@ -5,12 +5,20 @@ import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
+import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
+import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey
+import ac.mdiq.podcini.net.sync.model.EpisodeAction
+import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
import ac.mdiq.podcini.playback.base.InTheatre
+import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UserPreferences
+import ac.mdiq.podcini.storage.database.Episodes.setPlayState
+import ac.mdiq.podcini.storage.database.Queues.addToQueue
+import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
@@ -25,11 +33,14 @@ import ac.mdiq.podcini.ui.actions.*
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme
+import ac.mdiq.podcini.ui.dialog.ShareDialog
+import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment.EpisodeHomeFragment.Companion
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
+import ac.mdiq.podcini.util.IntentUtils
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import android.content.Context
@@ -57,6 +68,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@@ -110,16 +122,14 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var itemLink by mutableStateOf("")
var hasMedia by mutableStateOf(true)
var rating by mutableStateOf(episode?.rating ?: 0)
+ var inQueue by mutableStateOf(if (episode != null) curQueue.contains(episode!!) else false)
+ var isPlayed by mutableStateOf(episode?.isPlayed() ?: false)
private var webviewData by mutableStateOf("")
private lateinit var shownotesCleaner: ShownotesCleaner
private lateinit var toolbar: MaterialToolbar
// private lateinit var webvDescription: ShownotesWebView
-// private lateinit var imgvCover: ImageView
-
-// private lateinit var butAction1: ImageView
-// private lateinit var butAction2: ImageView
private var actionButton1 by mutableStateOf(null)
private var actionButton2 by mutableStateOf(null)
@@ -207,17 +217,49 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column {
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) {
val imgLoc = if (episode != null) ImageResourceUtils.getEpisodeListImageLocation(episode!!) else null
- AsyncImage(model = imgLoc, contentDescription = "imgvCover", Modifier.width(56.dp).height(56.dp).clickable(onClick = { openPodcast() }))
+ AsyncImage(model = imgLoc, contentDescription = "imgvCover", error = painterResource(R.mipmap.ic_launcher), modifier = Modifier.width(56.dp).height(56.dp).clickable(onClick = { openPodcast() }))
Column(modifier = Modifier.padding(start = 10.dp)) {
Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.clickable { openPodcast() })
Text(txtvTitle, color = textColor, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), maxLines = 5, overflow = TextOverflow.Ellipsis)
Text("$txtvPublished · $txtvDuration · $txtvSize", color = textColor, style = MaterialTheme.typography.bodyMedium)
}
}
- Row(verticalAlignment = Alignment.CenterVertically) {
+ Row(modifier = Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.weight(0.4f))
- var ratingIconRes = Episode.Rating.fromCode(rating).res
- Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.width(15.dp).height(15.dp).clickable(onClick = {
+ val playedIconRes = if (isPlayed) R.drawable.ic_mark_unplayed else R.drawable.ic_mark_played
+ Icon(painter = painterResource(playedIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "isPlayed", modifier = Modifier.width(24.dp).height(24.dp)
+ .clickable(onClick = {
+ if (isPlayed) {
+ setPlayState(Episode.PlayState.UNPLAYED.code, false, episode!!)
+ if (isProviderConnected && episode?.feed?.isLocalFeed != true && episode?.media != null) {
+ val actionNew: EpisodeAction = EpisodeAction.Builder(episode!!, EpisodeAction.NEW).currentTimestamp().build()
+ SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(requireContext(), actionNew)
+ }
+ } else {
+ setPlayState(Episode.PlayState.PLAYED.code, true, episode!!)
+ if (episode?.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) {
+ val media: EpisodeMedia? = episode?.media
+ // not all items have media, Gpodder only cares about those that do
+ if (isProviderConnected && media != null) {
+ val actionPlay: EpisodeAction = EpisodeAction.Builder(episode!!, EpisodeAction.PLAY)
+ .currentTimestamp()
+ .started(media.getDuration() / 1000)
+ .position(media.getDuration() / 1000)
+ .total(media.getDuration() / 1000)
+ .build()
+ SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(requireContext(), actionPlay)
+ }
+ }
+ }
+ }))
+ Spacer(modifier = Modifier.weight(0.2f))
+ val inQueueIconRes = if (!inQueue && episode?.media != null) R.drawable.ic_playlist_play else R.drawable.ic_playlist_remove
+ Icon(painter = painterResource(inQueueIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "inQueue", modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = {
+ if (inQueue) removeFromQueue(episode!!) else addToQueue(true, episode!!)
+ }))
+ Spacer(modifier = Modifier.weight(0.2f))
+ val ratingIconRes = Episode.Rating.fromCode(rating).res
+ Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = {
showChooseRatingDialog = true
}))
Spacer(modifier = Modifier.weight(0.2f))
@@ -347,9 +389,22 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
return true
}
+ R.id.visit_website_item -> {
+ val url = episode?.getLinkWithFallback()
+ if (url != null) IntentUtils.openInBrowser(requireContext(), url)
+ return true
+ }
+ R.id.share_item -> {
+ if (episode != null) {
+ val shareDialog: ShareDialog = ShareDialog.newInstance(episode!!)
+ shareDialog.show((requireActivity().supportFragmentManager), "ShareEpisodeDialog")
+ }
+ return true
+ }
else -> {
- if (episode == null) return false
- return EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, episode!!)
+ return true
+// if (episode == null) return false
+// return EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, episode!!)
}
}
}
@@ -382,11 +437,11 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
updateAppearance()
}
- private fun prepareMenu() {
- if (episode!!.media != null) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast)
- // these are already available via button1 and button2
- else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
- }
+// private fun prepareMenu() {
+// if (episode!!.media != null) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast)
+// // these are already available via button1 and button2
+// else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
+// }
@UnstableApi
private fun updateAppearance() {
@@ -394,7 +449,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "updateAppearance item is null")
return
}
- prepareMenu()
+// prepareMenu()
if (episode!!.feed != null) txtvPodcast = episode!!.feed!!.title ?: ""
txtvTitle = episode!!.title ?:""
@@ -540,7 +595,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
episode!!.rating = event.rating
rating = episode!!.rating
// episode = event.episode
- prepareMenu()
+// prepareMenu()
}
}
@@ -550,7 +605,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
while (i < size) {
val item_ = event.episodes[i]
if (item_.id == episode?.id) {
- prepareMenu()
+ inQueue = curQueue.contains(episode!!)
+// prepareMenu()
break
}
i++
@@ -584,6 +640,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
+ if (episode != null) episode = realm.query(Episode::class).query("id == $0", episode!!.id).first().find()
if (episode != null) {
val duration = episode!!.media?.getDuration() ?: Int.MAX_VALUE
Logd(TAG, "description: ${episode?.description}")
@@ -603,6 +660,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
withContext(Dispatchers.Main) {
// binding.progbarLoading.visibility = View.GONE
rating = episode!!.rating
+ inQueue = curQueue.contains(episode!!)
+ isPlayed = episode!!.isPlayed()
onFragmentLoaded()
itemLoaded = true
}
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 096dd678..1b5d2ecb 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
@@ -246,8 +246,8 @@ import java.util.concurrent.Semaphore
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
})
- AsyncImage(model = feed?.imageUrl?:"", contentDescription = "imgvCover",
- Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) {
+ AsyncImage(model = feed?.imageUrl?:"", contentDescription = "imgvCover", error = painterResource(R.mipmap.ic_launcher),
+ modifier = Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) {
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}.clickable(onClick = {
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 6bfe4c8e..59016a9e 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
@@ -155,7 +155,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}))
Spacer(modifier = Modifier.weight(0.2f))
Button(onClick = { (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id)) }) {
- Text(feed.episodes.size.toString() + " " + stringResource(R.string.episodes_label), color = textColor)
+ Text(feed.episodes.size.toString() + " " + stringResource(R.string.episodes_label))
}
Spacer(modifier = Modifier.width(15.dp))
}
@@ -169,8 +169,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
})
- AsyncImage(model = feed.imageUrl?:"", contentDescription = "imgvCover",
- Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) {
+ AsyncImage(model = feed.imageUrl?:"", contentDescription = "imgvCover", error = painterResource(R.mipmap.ic_launcher),
+ modifier = Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) {
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}.clickable(onClick = {
@@ -240,7 +240,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val fragment = SearchResultsFragment.newInstance(CombinedSearcher::class.java, "$txtvAuthor podcasts")
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
}) {
- Text(stringResource(R.string.feeds_related_to_author), color = textColor)
+ Text(stringResource(R.string.feeds_related_to_author))
}
Text(stringResource(R.string.statistics_label), color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp))
val arguments = Bundle()
@@ -250,7 +250,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Button({
(activity as MainActivity).loadChildFragment(StatisticsFragment(), TransitionEffect.SLIDE)
}) {
- Text(stringResource(R.string.statistics_view_all), color = textColor)
+ Text(stringResource(R.string.statistics_view_all))
}
}
}
@@ -266,7 +266,6 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "Language: ${feed.language} Author: ${feed.author}")
Logd(TAG, "URL: ${feed.downloadUrl}")
// TODO: need to generate blurred image for background
-
refreshToolbarState()
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt
index 87b7a172..fe27eb02 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt
@@ -10,6 +10,8 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeFilter.Companion.unfiltered
import ac.mdiq.podcini.storage.model.Feed
+import ac.mdiq.podcini.storage.model.PlayQueue
+import ac.mdiq.podcini.storage.model.ShareLog
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
@@ -136,7 +138,8 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
(activity as MainActivity).loadFragment(FeedEpisodesFragment.TAG, args)
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
}) {
- AsyncImage(model = f.imageUrl, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), modifier = Modifier.width(40.dp).height(40.dp))
+ AsyncImage(model = f.imageUrl, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
+ modifier = Modifier.width(40.dp).height(40.dp))
Text(f.title?:"No title", color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(start = 10.dp))
}
}
@@ -246,19 +249,14 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
*/
fun getDatasetStats() {
Logd(TAG, "getNavDrawerData() called")
- val numDownloadedItems = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
val numItems = getEpisodesCount(unfiltered())
feedCount = getFeedCount()
- while (curQueue.name.isEmpty()) runBlocking { delay(100) }
- val queueSize = curQueue.episodeIds.size
- Logd(TAG, "getDatasetStats: queueSize: $queueSize")
- val historyCount = getNumberOfPlayed().toInt()
- navMap[QueuesFragment.TAG]?.count = queueSize
+ navMap[QueuesFragment.TAG]?.count = realm.query(PlayQueue::class).find().sumOf { it.size()}
navMap[SubscriptionsFragment.TAG]?.count = feedCount
- navMap[HistoryFragment.TAG]?.count = historyCount
- navMap[DownloadsFragment.TAG]?.count = numDownloadedItems
- navMap[AllEpisodesFragment.TAG]?.count = numItems
+ navMap[HistoryFragment.TAG]?.count = getNumberOfPlayed().toInt()
+ navMap[DownloadsFragment.TAG]?.count = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
navMap[AllEpisodesFragment.TAG]?.count = numItems
+ navMap[SharedLogFragment.TAG]?.count = realm.query(ShareLog::class).count().find().toInt()
}
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
index 22f8723c..57447001 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt
@@ -84,7 +84,7 @@ class OnlineFeedFragment : Fragment() {
private var autoDownloadChecked by mutableStateOf(false)
private var enableSubscribe by mutableStateOf(true)
private var enableEpisodes by mutableStateOf(true)
- private var subButTextRes by mutableIntStateOf(R.string.subscribing_label)
+ private var subButTextRes by mutableIntStateOf(R.string.subscribe_label)
private val feedId: Long
get() {
@@ -339,8 +339,8 @@ class OnlineFeedFragment : Fragment() {
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
})
- AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage",
- Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) {
+ AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher),
+ modifier = Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) {
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
}.clickable(onClick = {}))
@@ -440,7 +440,7 @@ class OnlineFeedFragment : Fragment() {
// }
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
enableSubscribe = false
- subButTextRes = R.string.subscribing_label
+ subButTextRes = R.string.subscribe_label
}
feedInFeedlist() -> {
enableSubscribe = true
@@ -470,7 +470,7 @@ class OnlineFeedFragment : Fragment() {
}
else -> {
enableSubscribe = true
- subButTextRes = R.string.subscribing_label
+ subButTextRes = R.string.subscribe_label
}
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt
index 542e422c..b08f2f21 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt
@@ -342,7 +342,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
}
loadToplist(countryCode)
}, ) {
- Text(stringResource(id = R.string.retry_label), color = textColor)
+ Text(stringResource(id = R.string.retry_label))
}
// Text( getString(R.string.search_powered_by, searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background(
// Color.LightGray)
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt
index b22f3568..968fc068 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt
@@ -112,7 +112,7 @@ class SearchResultsFragment : Fragment() {
if (searchResults.isEmpty()) Text(noResultText, color = textColor, modifier = Modifier.constrainAs(empty) { centerTo(parent) })
if (errorText.isNotEmpty()) Text(errorText, color = textColor, modifier = Modifier.constrainAs(txtvError) { centerTo(parent) })
if (retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom)}, onClick = { search(retryQerry) }, ) {
- Text(stringResource(id = R.string.retry_label), color = textColor)
+ Text(stringResource(id = R.string.retry_label))
}
Text( getString(R.string.search_powered_by, searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background(Color.LightGray)
.constrainAs(powered) {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SharedLogFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SharedLogFragment.kt
index 6e204d6d..162040c1 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SharedLogFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SharedLogFragment.kt
@@ -160,8 +160,8 @@ class SharedLogFragment : Fragment(), Toolbar.OnMenuItemClickListener {
try {
val result = withContext(Dispatchers.IO) {
Logd(TAG, "getDownloadLog() called")
- val dlog = realm.query(ShareLog::class).sort("id", Sort.DESCENDING).find().toMutableList()
- realm.copyFromRealm(dlog)
+ realm.query(ShareLog::class).sort("id", Sort.DESCENDING).find().toMutableList()
+// realm.copyFromRealm(dlog)
}
withContext(Dispatchers.Main) {
logs.clear()
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 62f7a94b..5cfd4a6e 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
@@ -3,10 +3,12 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.*
import ac.mdiq.podcini.net.feed.FeedUpdateManager
+import ac.mdiq.podcini.playback.base.VideoMode
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.fragments.ImportExportPreferencesFragment.*
+import ac.mdiq.podcini.storage.database.Feeds.createSynthetic
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.getTags
import ac.mdiq.podcini.storage.database.RealmDB.realm
@@ -18,6 +20,7 @@ import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOpt
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.Spinner
+import ac.mdiq.podcini.ui.dialog.CustomFeedNameDialog
import ac.mdiq.podcini.ui.dialog.FeedSortDialog
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
@@ -27,6 +30,7 @@ import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
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
@@ -311,6 +315,18 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
R.id.subscriptions_filter -> FeedFilterDialog.newInstance(FeedFilter(feedsFilter)).show(childFragmentManager, null)
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
R.id.subscriptions_sort -> FeedSortDialog().show(childFragmentManager, "FeedSortDialog")
+ R.id.new_synth -> {
+ val feed = createSynthetic(0, "")
+ feed.type = Feed.FeedType.RSS.name
+ CustomFeedNameDialog(activity as Activity, feed).show()
+ }
+ R.id.new_synth_yt -> {
+ val feed = createSynthetic(0, "")
+ feed.type = Feed.FeedType.YOUTUBE.name
+// feed.hasVideoMedia = video
+// feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
+ CustomFeedNameDialog(activity as Activity, feed).show()
+ }
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext())
R.id.toggle_grid_list -> useGrid = if (useGrid == null) !useGridLayout else !useGrid!!
else -> return false
@@ -843,7 +859,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val textColor = MaterialTheme.colorScheme.onSurface
ConstraintLayout {
val (coverImage, episodeCount, error) = createRefs()
- AsyncImage(model = feed.imageUrl, contentDescription = "coverImage", placeholder = painterResource(R.mipmap.ic_launcher),
+ AsyncImage(model = feed.imageUrl, contentDescription = "coverImage",
+ placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier
.constrainAs(coverImage) {
top.linkTo(parent.top)
@@ -882,7 +899,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "toggleSelected: selected: ${selected.size}")
}
Row(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
- AsyncImage(model = feed.imageUrl, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher),
+ AsyncImage(model = feed.imageUrl, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(80.dp).height(80.dp)
.clickable(onClick = {
Logd(TAG, "icon clicked!")
diff --git a/app/src/main/res/drawable/baseline_category_24.xml b/app/src/main/res/drawable/baseline_category_24.xml
new file mode 100644
index 00000000..76b250d0
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_category_24.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_shelves_24.xml b/app/src/main/res/drawable/baseline_shelves_24.xml
new file mode 100644
index 00000000..ddbea23e
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_shelves_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/menu/episode_info.xml b/app/src/main/res/menu/episode_info.xml
index 3f7f8677..5eda4cdb 100644
--- a/app/src/main/res/menu/episode_info.xml
+++ b/app/src/main/res/menu/episode_info.xml
@@ -2,55 +2,6 @@
diff --git a/app/src/main/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml
index afd13112..dfdaa4c5 100644
--- a/app/src/main/res/menu/mediaplayer.xml
+++ b/app/src/main/res/menu/mediaplayer.xml
@@ -18,19 +18,6 @@
custom:showAsAction="always">
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
- Error
An error occurred:
Refresh
+ New synthetic feed
+ New synthetic Youtube
Toggle grid list
Refreshing
Reconcile
@@ -158,6 +160,8 @@
Add to queue…
Remove from other queues
+ Remove from current feed
+
Nothing
Never
When not favorited
@@ -274,6 +278,7 @@
- %d episode marked as unplayed.
- %d episodes marked as unplayed.
+ Shelve to synthetic
Add to active queue
- %d episode added to queue.
diff --git a/changelog.md b/changelog.md
index 16023c41..39f56761 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,20 @@
+# 6.10.0
+
+* in Subscriptions, added menu items to create normal or Youtube synthetic feeds for better organization
+* added "Shelve to synthetic" in multi-selection menu to move/copy the selected to a synthetic feed
+ * episodes from normal podcasts can only be copied, while those from synthetic podcasts can be moved
+* clicking on the image in Player UI toggles expand and collapse of the player detailed view
+* when receiving shared single media from Youtube, wait for episode construction before dismissing the confirm dialog
+* fixed Reconcile crash when episode.media is null
+* in OnlineFeed, button "Subscribing" is changed to "Subscribe"
+* tunes color contrast on some Compose buttons
+* in EpisodeInfo, menu items "mark played" and "add to queue" are made as buttons and telltales
+* cleaned up menu items handling in EpisodeInfo and AudioPlayer, removed EpisodeMenuHandler
+* fixed a bug of episode properties possibly getting overwritten when changing episode play status
+* in NavDrawer, the count for Queues is from all queues (previously from curQueue only)
+* count of shared logs is shown on NavDrawer
+* set app icon as default when cover images are unavailable
+
# 6.9.3
* fixed app quit issue when repairing a shared item
diff --git a/fastlane/metadata/android/en-US/changelogs/3020269.txt b/fastlane/metadata/android/en-US/changelogs/3020269.txt
new file mode 100644
index 00000000..46d695c7
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3020269.txt
@@ -0,0 +1,16 @@
+ Version 6.10.0
+
+* in Subscriptions, added menu items to create normal or Youtube synthetic feeds for better organization
+* added "Shelve to synthetic" in multi-selection menu to move/copy the selected to a synthetic feed
+ * episodes from normal podcasts can only be copied, while those from synthetic podcasts can be moved
+* clicking on the image in Player UI toggles expand and collapse of the player detailed view
+* when receiving shared single media from Youtube, wait for episode construction before dismissing the confirm dialog
+* fixed Reconcile crash when episode.media is null
+* in OnlineFeed, button "Subscribing" is changed to "Subscribe"
+* tunes color contrast on some Compose buttons
+* in EpisodeInfo, menu items "mark played" and "add to queue" are made as buttons and telltales
+* cleaned up menu items handling in EpisodeInfo and AudioPlayer, removed EpisodeMenuHandler
+* fixed a bug of episode properties possibly getting overwritten when changing episode play status
+* in NavDrawer, the count for Queues is from all queues (previously from curQueue only)
+* count of shared logs is shown on NavDrawer
+* set app icon as default when cover images are unavailable