From 9ce9b3f5b6f1ad564942e4f0317f4494b0bf52a7 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:26:59 +0100 Subject: [PATCH] 6.10.0 commit --- README.md | 10 +- app/build.gradle | 4 +- .../mdiq/podcini/storage/database/Episodes.kt | 21 +- .../ac/mdiq/podcini/storage/database/Feeds.kt | 46 ++-- .../podcini/ui/actions/EpisodeMenuHandler.kt | 196 ------------------ .../mdiq/podcini/ui/actions/MenuItemUtils.kt | 1 - .../ui/activity/ShareReceiverActivity.kt | 2 +- .../ui/activity/VideoplayerActivity.kt | 1 - .../ac/mdiq/podcini/ui/compose/EpisodesVM.kt | 141 +++++++++---- .../ac/mdiq/podcini/ui/compose/OnlineFeed.kt | 5 +- .../podcini/ui/dialog/CustomFeedNameDialog.kt | 7 +- .../ui/fragment/AudioPlayerFragment.kt | 57 ++--- .../podcini/ui/fragment/DownloadsFragment.kt | 2 +- .../ui/fragment/EpisodeInfoFragment.kt | 95 +++++++-- .../ui/fragment/FeedEpisodesFragment.kt | 4 +- .../podcini/ui/fragment/FeedInfoFragment.kt | 11 +- .../podcini/ui/fragment/NavDrawerFragment.kt | 18 +- .../podcini/ui/fragment/OnlineFeedFragment.kt | 10 +- .../ui/fragment/QuickDiscoveryFragment.kt | 2 +- .../ui/fragment/SearchResultsFragment.kt | 2 +- .../podcini/ui/fragment/SharedLogFragment.kt | 4 +- .../ui/fragment/SubscriptionsFragment.kt | 21 +- .../res/drawable/baseline_category_24.xml | 9 + .../main/res/drawable/baseline_shelves_24.xml | 5 + app/src/main/res/menu/episode_info.xml | 55 ----- app/src/main/res/menu/mediaplayer.xml | 13 -- app/src/main/res/menu/subscriptions.xml | 8 + app/src/main/res/values/strings.xml | 5 + changelog.md | 17 ++ .../android/en-US/changelogs/3020269.txt | 16 ++ 30 files changed, 369 insertions(+), 419 deletions(-) delete mode 100644 app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeMenuHandler.kt create mode 100644 app/src/main/res/drawable/baseline_category_24.xml create mode 100644 app/src/main/res/drawable/baseline_shelves_24.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/3020269.txt 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 [Amazon](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13) [OpenAPK](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