6.10.0 commit

This commit is contained in:
Xilin Jia 2024-10-10 15:26:59 +01:00
parent 971a45c1d6
commit 9ce9b3f5b6
30 changed files with 369 additions and 419 deletions

View File

@ -12,6 +12,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
[<img src="./images/external/amazon.png" alt="Amazon" height="40">](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13) [<img src="./images/external/amazon.png" alt="Amazon" height="40">](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13)
[<img src="./images/external/getItOpenapk.png" alt="OpenAPK" height="50">](https://www.openapk.net/podcini/ac.mdiq.podcini/) [<img src="./images/external/getItOpenapk.png" alt="OpenAPK" height="50">](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 #### 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) That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs)
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions. #### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
@ -29,10 +30,11 @@ Compared to AntennaPod this project:
4. Boasts new UI's including streamlined drawer, subscriptions view and player controller, 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 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 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, 7. Features synthetic podcasts and allows episodes to be shelved to any synthetic podcast
8. Allows adding personal notes and 5-level rating on every episode 8. Supports channels, playlists, single media from YouTube and YT Music, as well as normal podcasts and plain RSS,
9. Offers Readability and Text-to-Speech for RSS contents,s 9. Allows adding personal notes and 5-level rating on every episode
10. Features `instant sync` across devices without a server. 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. The project aims to profit from modern frameworks, improve efficiency and provide more useful and user-friendly features.

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020268 versionCode 3020269
versionName "6.9.3" versionName "6.10.0"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -39,6 +39,7 @@ import androidx.annotation.OptIn
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import io.realm.kotlin.ext.isManaged
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -247,14 +248,14 @@ object Episodes {
} }
} }
@JvmStatic // @JvmStatic
fun setFavorite(episode: Episode, stat: Boolean?) : Job { // fun setFavorite(episode: Episode, stat: Boolean?) : Job {
Logd(TAG, "setFavorite called $stat") // Logd(TAG, "setFavorite called $stat")
return runOnIOScope { // return runOnIOScope {
val result = upsert(episode) { it.rating = if (stat ?: !it.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code } // 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)) // EventFlow.postEvent(FlowEvent.RatingEvent(result, result.rating))
} // }
} // }
fun setRating(episode: Episode, rating: Int) : Job { fun setRating(episode: Episode, rating: Int) : Job {
Logd(TAG, "setRating called $rating") Logd(TAG, "setRating called $rating")
@ -283,7 +284,9 @@ object Episodes {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode { suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode {
Logd(TAG, "setPlayStateSync called played: $played resetMediaPosition: $resetMediaPosition ${episode.title}") 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 if (played >= PlayState.NEW.code && played <= PlayState.BUILDING.code) it.playState = played
else { else {
if (it.playState == PlayState.PLAYED.code) it.playState = PlayState.UNPLAYED.code if (it.playState == PlayState.PLAYED.code) it.playState = PlayState.UNPLAYED.code

View File

@ -423,17 +423,11 @@ object Feeds {
var feed = getFeed(feedId, true) var feed = getFeed(feedId, true)
if (feed != null) return feed if (feed != null) return feed
feed = Feed() val name = if (music) "YTMusic Syndicate" + if (video) "" else " Audio"
feed.id = feedId else "Youtube Syndicate" + if (video) "" else " Audio"
if (music) feed.title = "YTMusic Syndicate" + if (video) "" else " Audio" feed = createSynthetic(feedId, name)
else feed.title = "Youtube Syndicate" + if (video) "" else " Audio"
feed.type = Feed.FeedType.YOUTUBE.name feed.type = Feed.FeedType.YOUTUBE.name
feed.hasVideoMedia = video 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 feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
upsertBlk(feed) {} upsertBlk(feed) {}
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED)) EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
@ -456,21 +450,35 @@ object Feeds {
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false)) EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(false))
} }
private fun getMiscSyndicate(): Feed { fun createSynthetic(feedId: Long, name: String): Feed {
var feedId: Long = 11 val feed = Feed()
var feed = getFeed(feedId, true) var feedId_ = feedId
if (feed != null) return feed if (feedId_ <= 0) {
var i = 100L
feed = Feed() while (true) {
feed.id = feedId if (getFeed(i++) != null) continue
feed.title = "Misc Syndicate" feedId_ = --i
feed.type = Feed.FeedType.RSS.name break
}
}
feed.id = feedId_
feed.title = name
feed.author = "Yours Truly"
feed.downloadUrl = null feed.downloadUrl = null
feed.fileUrl = File(feedfilePath, getFeedfileName(feed)).toString() feed.fileUrl = File(feedfilePath, getFeedfileName(feed)).toString()
feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
feed.preferences!!.keepUpdated = false feed.preferences!!.keepUpdated = false
feed.preferences!!.queueId = -2L 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) {} upsertBlk(feed) {}
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED)) EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED))
return feed return feed

View File

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

View File

@ -13,7 +13,6 @@ object MenuItemUtils {
* context menu was created from. This assigns the listener to every menu item, * 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. * 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 * Note that Android still calls the onContextItemSelected methods of all fragments
* when the passed listener returns false. * when the passed listener returns false.
*/ */

View File

@ -59,7 +59,7 @@ class ShareReceiverActivity : AppCompatActivity() {
CustomTheme(this) { CustomTheme(this) {
confirmAddYoutubeEpisode(listOf(sharedUrl!!), showDialog.value, onDismissRequest = { confirmAddYoutubeEpisode(listOf(sharedUrl!!), showDialog.value, onDismissRequest = {
showDialog.value = false showDialog.value = false
// finish() finish()
}) })
} }
} }

View File

@ -25,7 +25,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode 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.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia

View File

@ -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.Queues.removeFromQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.PlayQueue import ac.mdiq.podcini.storage.model.Feed.Companion.newId
import ac.mdiq.podcini.storage.model.ShareLog
import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.ImageResourceUtils import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.ui.actions.EpisodeActionButton import ac.mdiq.podcini.ui.actions.EpisodeActionButton
@ -248,6 +247,62 @@ fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
} }
} }
@Composable
fun ShelveDialog(selected: List<Episode>, 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<Feed?>(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<Episode> = 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) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>, refreshCB: (()->Unit)? = null, fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>, refreshCB: (()->Unit)? = null,
@ -273,6 +328,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
var showPutToQueueDialog by remember { mutableStateOf(false) } var showPutToQueueDialog by remember { mutableStateOf(false) }
if (showPutToQueueDialog) PutToQueueDialog(selected) { showPutToQueueDialog = false } if (showPutToQueueDialog) PutToQueueDialog(selected) { showPutToQueueDialog = false }
var showShelveDialog by remember { mutableStateOf(false) }
if (showShelveDialog) ShelveDialog(selected) { showShelveDialog = false }
@Composable @Composable
fun EpisodeSpeedDial(modifier: Modifier = Modifier) { fun EpisodeSpeedDial(modifier: Modifier = Modifier) {
var isExpanded by remember { mutableStateOf(false) } var isExpanded by remember { mutableStateOf(false) }
@ -284,7 +342,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
Logd(TAG, "ic_delete: ${selected.size}") Logd(TAG, "ic_delete: ${selected.size}")
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected) LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
}, verticalAlignment = Alignment.CenterVertically) { }, 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)) } }, Text(stringResource(id = R.string.delete_episode_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp) { Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable { .clickable {
@ -296,7 +354,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
?.download(activity, episode) ?.download(activity, episode)
} }
}, verticalAlignment = Alignment.CenterVertically) { }, 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)) } }, Text(stringResource(id = R.string.download_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp) { Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable { .clickable {
@ -305,7 +363,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
Logd(TAG, "ic_mark_played: ${selected.size}") Logd(TAG, "ic_mark_played: ${selected.size}")
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray()) setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
}, verticalAlignment = Alignment.CenterVertically) { }, 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)) } }, Text(stringResource(id = R.string.toggle_played_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp) { Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable { .clickable {
@ -314,7 +372,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
Logd(TAG, "ic_playlist_remove: ${selected.size}") Logd(TAG, "ic_playlist_remove: ${selected.size}")
removeFromQueue(*selected.toTypedArray()) removeFromQueue(*selected.toTypedArray())
}, verticalAlignment = Alignment.CenterVertically) { }, 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)) } }, Text(stringResource(id = R.string.remove_from_queue_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp) { Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable { .clickable {
@ -323,17 +381,25 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
Logd(TAG, "ic_playlist_play: ${selected.size}") Logd(TAG, "ic_playlist_play: ${selected.size}")
Queues.addToQueue(true, *selected.toTypedArray()) Queues.addToQueue(true, *selected.toTypedArray())
}, verticalAlignment = Alignment.CenterVertically) { }, 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)) } }, 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) { Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable { .clickable {
isExpanded = false isExpanded = false
selectMode = false selectMode = false
Logd(TAG, "ic_playlist_play: ${selected.size}") Logd(TAG, "ic_playlist_play: ${selected.size}")
showPutToQueueDialog = true showPutToQueueDialog = true
// PutToQueueDialog(activity, selected).show()
}, verticalAlignment = Alignment.CenterVertically) { }, 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)) } }, Text(stringResource(id = R.string.put_in_queue_label)) } },
{ Row(modifier = Modifier.padding(horizontal = 16.dp) { Row(modifier = Modifier.padding(horizontal = 16.dp)
.clickable { .clickable {
@ -342,7 +408,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
showChooseRatingDialog = true showChooseRatingDialog = true
isExpanded = false isExpanded = false
}, verticalAlignment = Alignment.CenterVertically) { }, 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)) } }, Text(stringResource(id = R.string.set_rating_label)) } },
) )
if (selected.isNotEmpty() && selected[0].isRemote.value) if (selected.isNotEmpty() && selected[0].isRemote.value)
@ -367,7 +433,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
} }
} }
}, verticalAlignment = Alignment.CenterVertically) { }, verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.AddCircle, "") Icon(Icons.Filled.AddCircle, "Reserve episodes")
Text(stringResource(id = R.string.reserve_episodes_label)) Text(stringResource(id = R.string.reserve_episodes_label))
} }
} }
@ -458,8 +524,10 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs() val (imgvCover, checkMark) = createRefs()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode) val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(vm.episode)
Logd(TAG, "imgLoc: $imgLoc")
AsyncImage(model = imgLoc, contentDescription = "imgvCover", AsyncImage(model = imgLoc, contentDescription = "imgvCover",
placeholder = painterResource(R.mipmap.ic_launcher), placeholder = painterResource(R.mipmap.ic_launcher),
error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(56.dp).height(56.dp) modifier = Modifier.width(56.dp).height(56.dp)
.constrainAs(imgvCover) { .constrainAs(imgvCover) {
top.linkTo(parent.top) top.linkTo(parent.top)
@ -614,39 +682,38 @@ fun confirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
if (showDialog) { if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { Dialog(onDismissRequest = { onDismissRequest() }) {
Card( Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
modifier = Modifier
.wrapContentSize(align = Alignment.Center)
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
var audioOnly by remember { mutableStateOf(false) } var audioOnly by remember { mutableStateOf(false) }
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Checkbox(checked = audioOnly, onCheckedChange = { audioOnly = it }) Checkbox(checked = audioOnly, onCheckedChange = { audioOnly = it })
Text(text = stringResource(R.string.pref_video_mode_audio_only), style = MaterialTheme.typography.bodyLarge.merge()) Text(text = stringResource(R.string.pref_video_mode_audio_only), style = MaterialTheme.typography.bodyLarge.merge())
} }
Button(onClick = { var showComfirmButton by remember { mutableStateOf(true) }
CoroutineScope(Dispatchers.IO).launch { if (showComfirmButton) {
for (url in sharedUrls) { Button(onClick = {
val log = realm.query(ShareLog::class).query("url == $0", url).first().find() showComfirmButton = false
try { CoroutineScope(Dispatchers.IO).launch {
val info = StreamInfo.getInfo(Vista.getService(0), url) for (url in sharedUrls) {
val episode = episodeFromStreamInfo(info) val log = realm.query(ShareLog::class).query("url == $0", url).first().find()
addToYoutubeSyndicate(episode, !audioOnly) try {
if (log != null) upsert(log) { it.status = 1 } val info = StreamInfo.getInfo(Vista.getService(0), url)
} catch (e: Throwable) { val episode = episodeFromStreamInfo(info)
toastMassege = "Receive share error: ${e.message}" addToYoutubeSyndicate(episode, !audioOnly)
Log.e(TAG, toastMassege) if (log != null) upsert(log) { it.status = 1 }
if (log != null) upsert(log) { it.details = e.message?: "error" } } catch (e: Throwable) {
withContext(Dispatchers.Main) { showToast = true } 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() } else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
}) {
Text("Confirm")
}
} }
} }
} }

View File

@ -82,9 +82,8 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult) {
Row { Row {
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs() val (imgvCover, checkMark) = createRefs()
AsyncImage(model = feed.imageUrl, AsyncImage(model = feed.imageUrl, contentDescription = "imgvCover",
contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
placeholder = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.width(65.dp).height(65.dp).constrainAs(imgvCover) { modifier = Modifier.width(65.dp).height(65.dp).constrainAs(imgvCover) {
top.linkTo(parent.top) top.linkTo(parent.top)
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)

View File

@ -30,9 +30,10 @@ class CustomFeedNameDialog(activity: Activity, private var feed: Feed) {
.setTitle(R.string.rename_feed_label) .setTitle(R.string.rename_feed_label)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val newTitle = binding.editText.text.toString() val newTitle = binding.editText.text.toString()
feed = unmanaged(feed) // feed = unmanaged(feed)
feed.setCustomTitle1(newTitle) feed = upsertBlk(feed) {
feed = upsertBlk(feed) {} it.setCustomTitle1(newTitle)
}
} }
.setNeutralButton(R.string.reset, null) .setNeutralButton(R.string.reset, null)
.setNegativeButton(R.string.cancel_label, null) .setNegativeButton(R.string.cancel_label, null)

View File

@ -32,16 +32,14 @@ import ac.mdiq.podcini.storage.utils.ChapterUtils
import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.ImageResourceUtils import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter 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.MainActivity
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment.EpisodeHomeFragment.Companion.episode
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.EventFlow
@ -231,25 +229,26 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (curMedia == null) return if (curMedia == null) return
if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start() 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) modifier = Modifier.width(70.dp).height(70.dp).padding(start = 5.dp)
.clickable(onClick = { .clickable(onClick = {
Logd(TAG, "icon clicked!") Logd(TAG, "playerUiFragment icon was clicked")
Logd(TAG, "playerUiFragment was clicked") if (isCollapsed) {
val media = curMedia val media = curMedia
if (media != null) { if (media != null) {
val mediaType = media.getMediaType() val mediaType = media.getMediaType()
if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY
|| (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) { || (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) {
Logd(TAG, "popping as audio episode") Logd(TAG, "popping as audio episode")
ensureService() ensureService()
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else { } else {
Logd(TAG, "popping video activity") Logd(TAG, "popping video activity")
val intent = getPlayerActivityIntent(requireContext(), mediaType) val intent = getPlayerActivityIntent(requireContext(), mediaType)
startActivity(intent) startActivity(intent)
}
} }
} } else (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
})) }))
Spacer(Modifier.weight(0.1f)) Spacer(Modifier.weight(0.1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) { 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() })) 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 = { 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() { private fun setChapterDividers() {
if (currentMedia == null) return if (currentMedia == null) return
if (currentMedia!!.getChapters().isNotEmpty()) { if (currentMedia!!.getChapters().isNotEmpty()) {
val chapters: List<Chapter> = currentMedia!!.getChapters() val chapters: List<Chapter> = currentMedia!!.getChapters()
val dividerPos = FloatArray(chapters.size) val dividerPos = FloatArray(chapters.size)
for (i in chapters.indices) { for (i in chapters.indices) {
dividerPos[i] = chapters[i].start / curDurationFB.toFloat() dividerPos[i] = chapters[i].start / curDurationFB.toFloat()
} }
@ -929,7 +926,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private fun onRatingEvent(event: FlowEvent.RatingEvent) { private fun onRatingEvent(event: FlowEvent.RatingEvent) {
if (curEpisode?.id == event.episode.id) { if (curEpisode?.id == event.episode.id) {
rating = event.rating 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 val isEpisodeMedia = currentMedia is EpisodeMedia
toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia) toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia)
val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episodeOrFetch() else null 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 mediaType = curMedia?.getMediaType()
val notAudioOnly = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY 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 { override fun onMenuItemClick(menuItem: MenuItem): Boolean {
val media: Playable = curMedia ?: return false val media: Playable = curMedia ?: return false
val feedItem = if (media is EpisodeMedia) media.episodeOrFetch() else null 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 val itemId = menuItem.itemId
when (itemId) { when (itemId) {
@ -988,6 +985,12 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
context.startActivity(intent) context.startActivity(intent)
} }
} }
R.id.share_item -> {
if (currentItem != null) {
val shareDialog: ShareDialog = ShareDialog.newInstance(currentItem!!)
shareDialog.show((requireActivity().supportFragmentManager), "ShareEpisodeDialog")
}
}
else -> return false else -> return false
} }
return true return true

View File

@ -176,7 +176,7 @@ import java.util.*
val items = realm.query(Episode::class).query("media.episode == nil").find() val items = realm.query(Episode::class).query("media.episode == nil").find()
Logd(TAG, "number of episode with null backlink: ${items.size}") Logd(TAG, "number of episode with null backlink: ${items.size}")
for (item in items) { for (item in items) {
upsert(item) { it.media!!.episode = it } if (item.media != null ) upsert(item) { it.media!!.episode = it }
} }
nameEpisodeMap.clear() nameEpisodeMap.clear()
for (e in episodes) { for (e in episodes) {

View File

@ -5,12 +5,20 @@ import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient 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.fetchHtmlSource
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
import ac.mdiq.podcini.playback.base.InTheatre 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.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UsageStatistics import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UserPreferences 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.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged 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.activity.MainActivity
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme 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.ShownotesCleaner
import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.IntentUtils
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import android.content.Context import android.content.Context
@ -57,6 +68,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -110,16 +122,14 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var itemLink by mutableStateOf("") private var itemLink by mutableStateOf("")
var hasMedia by mutableStateOf(true) var hasMedia by mutableStateOf(true)
var rating by mutableStateOf(episode?.rating ?: 0) 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 var webviewData by mutableStateOf("")
private lateinit var shownotesCleaner: ShownotesCleaner private lateinit var shownotesCleaner: ShownotesCleaner
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
// private lateinit var webvDescription: ShownotesWebView // 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<EpisodeActionButton?>(null) private var actionButton1 by mutableStateOf<EpisodeActionButton?>(null)
private var actionButton2 by mutableStateOf<EpisodeActionButton?>(null) private var actionButton2 by mutableStateOf<EpisodeActionButton?>(null)
@ -207,17 +217,49 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column { Column {
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) {
val imgLoc = if (episode != null) ImageResourceUtils.getEpisodeListImageLocation(episode!!) else null 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)) { Column(modifier = Modifier.padding(start = 10.dp)) {
Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.clickable { openPodcast() }) 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(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) 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)) Spacer(modifier = Modifier.weight(0.4f))
var ratingIconRes = Episode.Rating.fromCode(rating).res val playedIconRes = if (isPlayed) R.drawable.ic_mark_unplayed else R.drawable.ic_mark_played
Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.width(15.dp).height(15.dp).clickable(onClick = { 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 showChooseRatingDialog = true
})) }))
Spacer(modifier = Modifier.weight(0.2f)) Spacer(modifier = Modifier.weight(0.2f))
@ -347,9 +389,22 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
return true 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 -> { else -> {
if (episode == null) return false return true
return EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, episode!!) // if (episode == null) return false
// return EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, episode!!)
} }
} }
} }
@ -382,11 +437,11 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
updateAppearance() updateAppearance()
} }
private fun prepareMenu() { // private fun prepareMenu() {
if (episode!!.media != null) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast) // if (episode!!.media != null) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast)
// these are already available via button1 and button2 // // 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) // else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
} // }
@UnstableApi @UnstableApi
private fun updateAppearance() { private fun updateAppearance() {
@ -394,7 +449,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "updateAppearance item is null") Logd(TAG, "updateAppearance item is null")
return return
} }
prepareMenu() // prepareMenu()
if (episode!!.feed != null) txtvPodcast = episode!!.feed!!.title ?: "" if (episode!!.feed != null) txtvPodcast = episode!!.feed!!.title ?: ""
txtvTitle = episode!!.title ?:"" txtvTitle = episode!!.title ?:""
@ -540,7 +595,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
episode!!.rating = event.rating episode!!.rating = event.rating
rating = episode!!.rating rating = episode!!.rating
// episode = event.episode // episode = event.episode
prepareMenu() // prepareMenu()
} }
} }
@ -550,7 +605,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
while (i < size) { while (i < size) {
val item_ = event.episodes[i] val item_ = event.episodes[i]
if (item_.id == episode?.id) { if (item_.id == episode?.id) {
prepareMenu() inQueue = curQueue.contains(episode!!)
// prepareMenu()
break break
} }
i++ i++
@ -584,6 +640,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (episode != null) episode = realm.query(Episode::class).query("id == $0", episode!!.id).first().find()
if (episode != null) { if (episode != null) {
val duration = episode!!.media?.getDuration() ?: Int.MAX_VALUE val duration = episode!!.media?.getDuration() ?: Int.MAX_VALUE
Logd(TAG, "description: ${episode?.description}") Logd(TAG, "description: ${episode?.description}")
@ -603,6 +660,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// binding.progbarLoading.visibility = View.GONE // binding.progbarLoading.visibility = View.GONE
rating = episode!!.rating rating = episode!!.rating
inQueue = curQueue.contains(episode!!)
isPlayed = episode!!.isPlayed()
onFragmentLoaded() onFragmentLoaded()
itemLoaded = true itemLoaded = true
} }

View File

@ -246,8 +246,8 @@ import java.util.concurrent.Semaphore
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
end.linkTo(parent.end) end.linkTo(parent.end)
}) })
AsyncImage(model = feed?.imageUrl?:"", contentDescription = "imgvCover", AsyncImage(model = feed?.imageUrl?:"", contentDescription = "imgvCover", error = painterResource(R.mipmap.ic_launcher),
Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) { modifier = Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) {
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
start.linkTo(parent.start) start.linkTo(parent.start)
}.clickable(onClick = { }.clickable(onClick = {

View File

@ -155,7 +155,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
})) }))
Spacer(modifier = Modifier.weight(0.2f)) Spacer(modifier = Modifier.weight(0.2f))
Button(onClick = { (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id)) }) { 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)) Spacer(modifier = Modifier.width(15.dp))
} }
@ -169,8 +169,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
end.linkTo(parent.end) end.linkTo(parent.end)
}) })
AsyncImage(model = feed.imageUrl?:"", contentDescription = "imgvCover", AsyncImage(model = feed.imageUrl?:"", contentDescription = "imgvCover", error = painterResource(R.mipmap.ic_launcher),
Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) { modifier = Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) {
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
start.linkTo(parent.start) start.linkTo(parent.start)
}.clickable(onClick = { }.clickable(onClick = {
@ -240,7 +240,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val fragment = SearchResultsFragment.newInstance(CombinedSearcher::class.java, "$txtvAuthor podcasts") val fragment = SearchResultsFragment.newInstance(CombinedSearcher::class.java, "$txtvAuthor podcasts")
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE) (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)) Text(stringResource(R.string.statistics_label), color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp))
val arguments = Bundle() val arguments = Bundle()
@ -250,7 +250,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Button({ Button({
(activity as MainActivity).loadChildFragment(StatisticsFragment(), TransitionEffect.SLIDE) (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, "Language: ${feed.language} Author: ${feed.author}")
Logd(TAG, "URL: ${feed.downloadUrl}") Logd(TAG, "URL: ${feed.downloadUrl}")
// TODO: need to generate blurred image for background // TODO: need to generate blurred image for background
refreshToolbarState() refreshToolbarState()
} }

View File

@ -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
import ac.mdiq.podcini.storage.model.EpisodeFilter.Companion.unfiltered import ac.mdiq.podcini.storage.model.EpisodeFilter.Companion.unfiltered
import ac.mdiq.podcini.storage.model.Feed 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.MainActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.compose.CustomTheme 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).loadFragment(FeedEpisodesFragment.TAG, args)
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED (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)) 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() { fun getDatasetStats() {
Logd(TAG, "getNavDrawerData() called") Logd(TAG, "getNavDrawerData() called")
val numDownloadedItems = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
val numItems = getEpisodesCount(unfiltered()) val numItems = getEpisodesCount(unfiltered())
feedCount = getFeedCount() feedCount = getFeedCount()
while (curQueue.name.isEmpty()) runBlocking { delay(100) } navMap[QueuesFragment.TAG]?.count = realm.query(PlayQueue::class).find().sumOf { it.size()}
val queueSize = curQueue.episodeIds.size
Logd(TAG, "getDatasetStats: queueSize: $queueSize")
val historyCount = getNumberOfPlayed().toInt()
navMap[QueuesFragment.TAG]?.count = queueSize
navMap[SubscriptionsFragment.TAG]?.count = feedCount navMap[SubscriptionsFragment.TAG]?.count = feedCount
navMap[HistoryFragment.TAG]?.count = historyCount navMap[HistoryFragment.TAG]?.count = getNumberOfPlayed().toInt()
navMap[DownloadsFragment.TAG]?.count = numDownloadedItems navMap[DownloadsFragment.TAG]?.count = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
navMap[AllEpisodesFragment.TAG]?.count = numItems
navMap[AllEpisodesFragment.TAG]?.count = numItems navMap[AllEpisodesFragment.TAG]?.count = numItems
navMap[SharedLogFragment.TAG]?.count = realm.query(ShareLog::class).count().find().toInt()
} }
} }
} }

View File

@ -84,7 +84,7 @@ class OnlineFeedFragment : Fragment() {
private var autoDownloadChecked by mutableStateOf(false) private var autoDownloadChecked by mutableStateOf(false)
private var enableSubscribe by mutableStateOf(true) private var enableSubscribe by mutableStateOf(true)
private var enableEpisodes 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 private val feedId: Long
get() { get() {
@ -339,8 +339,8 @@ class OnlineFeedFragment : Fragment() {
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
start.linkTo(parent.start) start.linkTo(parent.start)
}) })
AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage", AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher),
Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) { modifier = Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) {
bottom.linkTo(parent.bottom) bottom.linkTo(parent.bottom)
start.linkTo(parent.start) start.linkTo(parent.start)
}.clickable(onClick = {})) }.clickable(onClick = {}))
@ -440,7 +440,7 @@ class OnlineFeedFragment : Fragment() {
// } // }
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> { dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
enableSubscribe = false enableSubscribe = false
subButTextRes = R.string.subscribing_label subButTextRes = R.string.subscribe_label
} }
feedInFeedlist() -> { feedInFeedlist() -> {
enableSubscribe = true enableSubscribe = true
@ -470,7 +470,7 @@ class OnlineFeedFragment : Fragment() {
} }
else -> { else -> {
enableSubscribe = true enableSubscribe = true
subButTextRes = R.string.subscribing_label subButTextRes = R.string.subscribe_label
} }
} }
} }

View File

@ -342,7 +342,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
} }
loadToplist(countryCode) 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( // Text( getString(R.string.search_powered_by, searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background(
// Color.LightGray) // Color.LightGray)

View File

@ -112,7 +112,7 @@ class SearchResultsFragment : Fragment() {
if (searchResults.isEmpty()) Text(noResultText, color = textColor, modifier = Modifier.constrainAs(empty) { centerTo(parent) }) 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 (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) }, ) { 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) Text( getString(R.string.search_powered_by, searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background(Color.LightGray)
.constrainAs(powered) { .constrainAs(powered) {

View File

@ -160,8 +160,8 @@ class SharedLogFragment : Fragment(), Toolbar.OnMenuItemClickListener {
try { try {
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
Logd(TAG, "getDownloadLog() called") Logd(TAG, "getDownloadLog() called")
val dlog = realm.query(ShareLog::class).sort("id", Sort.DESCENDING).find().toMutableList() realm.query(ShareLog::class).sort("id", Sort.DESCENDING).find().toMutableList()
realm.copyFromRealm(dlog) // realm.copyFromRealm(dlog)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
logs.clear() logs.clear()

View File

@ -3,10 +3,12 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.* import ac.mdiq.podcini.databinding.*
import ac.mdiq.podcini.net.feed.FeedUpdateManager 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.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.fragments.ImportExportPreferencesFragment.* 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.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.getTags import ac.mdiq.podcini.storage.database.Feeds.getTags
import ac.mdiq.podcini.storage.database.RealmDB.realm 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.activity.MainActivity
import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.Spinner 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.FeedSortDialog
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog 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.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import android.app.Activity
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.app.Dialog import android.app.Dialog
import android.content.ActivityNotFoundException 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.subscriptions_filter -> FeedFilterDialog.newInstance(FeedFilter(feedsFilter)).show(childFragmentManager, null)
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
R.id.subscriptions_sort -> FeedSortDialog().show(childFragmentManager, "FeedSortDialog") 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.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext())
R.id.toggle_grid_list -> useGrid = if (useGrid == null) !useGridLayout else !useGrid!! R.id.toggle_grid_list -> useGrid = if (useGrid == null) !useGridLayout else !useGrid!!
else -> return false else -> return false
@ -843,7 +859,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
ConstraintLayout { ConstraintLayout {
val (coverImage, episodeCount, error) = createRefs() 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 modifier = Modifier
.constrainAs(coverImage) { .constrainAs(coverImage) {
top.linkTo(parent.top) top.linkTo(parent.top)
@ -882,7 +899,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Logd(TAG, "toggleSelected: selected: ${selected.size}") Logd(TAG, "toggleSelected: selected: ${selected.size}")
} }
Row(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) { 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) modifier = Modifier.width(80.dp).height(80.dp)
.clickable(onClick = { .clickable(onClick = {
Logd(TAG, "icon clicked!") Logd(TAG, "icon clicked!")

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2l-5.5,9h11z"/>
<path android:fillColor="@android:color/white" android:pathData="M17.5,17.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0"/>
<path android:fillColor="@android:color/white" android:pathData="M3,13.5h8v8H3z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,1v2H5V1H3v22h2v-2h14v2h2V1H19zM19,5v6h-6V7H7v4H5V5H19zM17,19v-4h-6v4H5v-6h14v6H17z"/>
</vector>

View File

@ -2,55 +2,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"> xmlns:custom="http://schemas.android.com/apk/res-auto">
<item
android:id="@id/skip_episode_item"
android:title="@string/skip_episode_label"
custom:showAsAction="collapseActionView">
</item>
<item
android:id="@+id/mark_read_item"
custom:showAsAction="always"
android:icon="@drawable/ic_mark_played"
android:title="@string/mark_read_label">
</item>
<item
android:id="@+id/mark_unread_item"
custom:showAsAction="always"
android:icon="@drawable/ic_mark_unplayed"
android:title="@string/mark_unread_label">
</item>
<item
android:id="@+id/add_to_queue_item"
custom:showAsAction="always"
android:icon="@drawable/ic_playlist_play"
android:title="@string/add_to_queue_label">
</item>
<item
android:id="@+id/remove_from_queue_item"
custom:showAsAction="always"
android:icon="@drawable/ic_playlist_remove"
android:title="@string/remove_from_queue_label">
</item>
<!-- <item-->
<!-- android:id="@+id/add_to_favorites_item"-->
<!-- android:icon="@drawable/ic_star_border"-->
<!-- custom:showAsAction="always"-->
<!-- android:title="@string/add_to_favorite_label" />-->
<!-- <item-->
<!-- android:id="@+id/remove_from_favorites_item"-->
<!-- android:icon="@drawable/ic_star"-->
<!-- custom:showAsAction="always"-->
<!-- android:title="@string/remove_from_favorite_label" />-->
<item
android:id="@+id/reset_position"
custom:showAsAction="collapseActionView"
android:title="@string/reset_position">
</item>
<item <item
android:id="@+id/visit_website_item" android:id="@+id/visit_website_item"
android:icon="@drawable/ic_web" android:icon="@drawable/ic_web"
@ -66,10 +17,4 @@
android:id="@+id/share_notes" android:id="@+id/share_notes"
android:title="@string/share_notes_label"> android:title="@string/share_notes_label">
</item> </item>
<item
android:id="@+id/open_podcast"
custom:showAsAction="collapseActionView"
android:title="@string/open_podcast">
</item>
</menu> </menu>

View File

@ -18,19 +18,6 @@
custom:showAsAction="always"> custom:showAsAction="always">
</item> </item>
<!-- <item-->
<!-- android:id="@+id/add_to_favorites_item"-->
<!-- android:icon="@drawable/ic_star_border"-->
<!-- android:title="@string/add_to_favorite_label"-->
<!-- custom:showAsAction="always">-->
<!-- </item>-->
<!-- <item-->
<!-- android:id="@+id/remove_from_favorites_item"-->
<!-- android:icon="@drawable/ic_star"-->
<!-- android:title="@string/remove_from_favorite_label"-->
<!-- custom:showAsAction="always">-->
<!-- </item>-->
<item <item
android:id="@+id/disable_sleeptimer_item" android:id="@+id/disable_sleeptimer_item"
android:icon="@drawable/ic_sleep_off" android:icon="@drawable/ic_sleep_off"

View File

@ -23,6 +23,14 @@
android:title="@string/statistics_label" android:title="@string/statistics_label"
android:visible="false" android:visible="false"
custom:showAsAction="always" /> custom:showAsAction="always" />
<item
android:id="@+id/new_synth"
android:title="@string/new_synth_label"
custom:showAsAction="never" />
<item
android:id="@+id/new_synth_yt"
android:title="@string/new_synth_yt_label"
custom:showAsAction="never" />
<item <item
android:id="@+id/refresh_item" android:id="@+id/refresh_item"
android:title="@string/refresh_label" android:title="@string/refresh_label"

View File

@ -120,6 +120,8 @@
<string name="error_label">Error</string> <string name="error_label">Error</string>
<string name="error_msg_prefix">An error occurred:</string> <string name="error_msg_prefix">An error occurred:</string>
<string name="refresh_label">Refresh</string> <string name="refresh_label">Refresh</string>
<string name="new_synth_label">New synthetic feed</string>
<string name="new_synth_yt_label">New synthetic Youtube</string>
<string name="toggle_grid_list">Toggle grid list</string> <string name="toggle_grid_list">Toggle grid list</string>
<string name="refreshing_label">Refreshing</string> <string name="refreshing_label">Refreshing</string>
<string name="reconcile_label">Reconcile</string> <string name="reconcile_label">Reconcile</string>
@ -158,6 +160,8 @@
<string name="put_in_queue_label">Add to queue…</string> <string name="put_in_queue_label">Add to queue…</string>
<string name="remove_from_other_queues">Remove from other queues</string> <string name="remove_from_other_queues">Remove from other queues</string>
<string name="remove_from_current_feed">Remove from current feed</string>
<string name="feed_new_episodes_action_nothing">Nothing</string> <string name="feed_new_episodes_action_nothing">Nothing</string>
<string name="episode_cleanup_never">Never</string> <string name="episode_cleanup_never">Never</string>
<string name="episode_cleanup_except_favorite_removal">When not favorited</string> <string name="episode_cleanup_except_favorite_removal">When not favorited</string>
@ -274,6 +278,7 @@
<item quantity="one">%d episode marked as unplayed.</item> <item quantity="one">%d episode marked as unplayed.</item>
<item quantity="other">%d episodes marked as unplayed.</item> <item quantity="other">%d episodes marked as unplayed.</item>
</plurals> </plurals>
<string name="shelve_label">Shelve to synthetic</string>
<string name="add_to_queue_label">Add to active queue</string> <string name="add_to_queue_label">Add to active queue</string>
<plurals name="added_to_queue_batch_label"> <plurals name="added_to_queue_batch_label">
<item quantity="one">%d episode added to queue.</item> <item quantity="one">%d episode added to queue.</item>

View File

@ -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 # 6.9.3
* fixed app quit issue when repairing a shared item * fixed app quit issue when repairing a shared item

View File

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