6.10.0 commit
This commit is contained in:
parent
971a45c1d6
commit
9ce9b3f5b6
10
README.md
10
README.md
|
@ -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/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
|
||||
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.
|
||||
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -59,7 +59,7 @@ class ShareReceiverActivity : AppCompatActivity() {
|
|||
CustomTheme(this) {
|
||||
confirmAddYoutubeEpisode(listOf(sharedUrl!!), showDialog.value, onDismissRequest = {
|
||||
showDialog.value = false
|
||||
// finish()
|
||||
finish()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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)
|
||||
@Composable
|
||||
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) }
|
||||
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<EpisodeVM>,
|
|||
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<EpisodeVM>,
|
|||
?.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<EpisodeVM>,
|
|||
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<EpisodeVM>,
|
|||
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<EpisodeVM>,
|
|||
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<EpisodeVM>,
|
|||
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<EpisodeVM>,
|
|||
}
|
||||
}
|
||||
}, 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<EpisodeVM>,
|
|||
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<String>, 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<Chapter> = 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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<EpisodeActionButton?>(null)
|
||||
private var actionButton2 by mutableStateOf<EpisodeActionButton?>(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
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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!")
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -2,55 +2,6 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:id="@+id/visit_website_item"
|
||||
android:icon="@drawable/ic_web"
|
||||
|
@ -66,10 +17,4 @@
|
|||
android:id="@+id/share_notes"
|
||||
android:title="@string/share_notes_label">
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/open_podcast"
|
||||
custom:showAsAction="collapseActionView"
|
||||
android:title="@string/open_podcast">
|
||||
</item>
|
||||
|
||||
</menu>
|
||||
|
|
|
@ -18,19 +18,6 @@
|
|||
custom:showAsAction="always">
|
||||
</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
|
||||
android:id="@+id/disable_sleeptimer_item"
|
||||
android:icon="@drawable/ic_sleep_off"
|
||||
|
|
|
@ -23,6 +23,14 @@
|
|||
android:title="@string/statistics_label"
|
||||
android:visible="false"
|
||||
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
|
||||
android:id="@+id/refresh_item"
|
||||
android:title="@string/refresh_label"
|
||||
|
|
|
@ -120,6 +120,8 @@
|
|||
<string name="error_label">Error</string>
|
||||
<string name="error_msg_prefix">An error occurred:</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="refreshing_label">Refreshing</string>
|
||||
<string name="reconcile_label">Reconcile</string>
|
||||
|
@ -158,6 +160,8 @@
|
|||
<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_current_feed">Remove from current feed</string>
|
||||
|
||||
<string name="feed_new_episodes_action_nothing">Nothing</string>
|
||||
<string name="episode_cleanup_never">Never</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="other">%d episodes marked as unplayed.</item>
|
||||
</plurals>
|
||||
<string name="shelve_label">Shelve to synthetic</string>
|
||||
<string name="add_to_queue_label">Add to active queue</string>
|
||||
<plurals name="added_to_queue_batch_label">
|
||||
<item quantity="one">%d episode added to queue.</item>
|
||||
|
|
17
changelog.md
17
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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue