6.7.2 commit

This commit is contained in:
Xilin Jia 2024-09-19 22:43:15 +01:00
parent daeee36985
commit bea7ce74a7
16 changed files with 156 additions and 91 deletions

View File

@ -12,7 +12,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
[<img src="./images/external/getItf-droid.png" alt="F-Droid" height="50">](https://f-droid.org/packages/ac.mdiq.podcini.R/)
[<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)
#### Podcini.R 6.6 introduces the powerful feature of synthetic podcasts, enables the receiving/handling shared single media as well as playlist from Youtube and YT Music, for more see the Youtube section below or the changelogs.
#### Podcini.R 6.6 introduces the powerful feature of synthetic podcasts, enables the receiving/handling shared single media as well as playlist and podcast from Youtube and YT Music, for more see the Youtube section below or the changelogs.
That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs)
#### Podcini.R version 6.5 as a major step forward brings YouTube channels in the app. They can be searched, received from share, subscribed and played from within Podcini. For more see the Youtube section below or the changelogs
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
@ -131,7 +131,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Youtube channels can be searched in podcast search view, can also be shared from other apps (such as Youtube) to Podcini
* Youtube channels can be subscribed as normal podcasts
* Playlists on Youtube or Youtube Music can be shared to Podcini, and then can be subscribed in similar fashion as the channels
* Playlists and podcasts on Youtube or Youtube Music can be shared to Podcini, and then can be subscribed in similar fashion as the channels
* Single media from Youtube or Youtube Music can also be shared from other apps, can be accepted as including video or audio only, are added to synthetic podcasts such as "Youtube Syndicate"
* All the media from Youtube or Youtube Music can be played (only streamed) with video in fullscreen and in window modes or in audio only mode in the background
* These media are played with the lowest video quality and highest audio quality

View File

@ -19,8 +19,8 @@ composeCompiler {
android {
defaultConfig {
minSdk 24
compileSdk 34
targetSdk 34
compileSdk 35
targetSdk 35
kotlinOptions {
jvmTarget = '17'
@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020254
versionName "6.7.1"
versionCode 3020255
versionName "6.7.2"
applicationId "ac.mdiq.podcini.R"
def commit = ""
@ -174,12 +174,12 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
def composeBom = platform('androidx.compose:compose-bom:2024.09.01')
def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.material:material:1.7.1'
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.1'
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.1'
implementation 'androidx.compose.material:material:1.7.2'
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.2'
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.2'
implementation 'androidx.activity:activity-compose:1.9.2'
implementation 'androidx.window:window:1.3.0'
@ -187,7 +187,7 @@ dependencies {
implementation "androidx.core:core-ktx:1.13.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.5"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6"
implementation "androidx.annotation:annotation:1.8.2"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
@ -205,7 +205,7 @@ dependencies {
implementation "androidx.work:work-runtime:2.9.1"
implementation "androidx.core:core-splashscreen:1.0.1"
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.webkit:webkit:1.11.0'
implementation 'androidx.webkit:webkit:1.12.0'
implementation "com.google.android.material:material:1.12.0"

View File

@ -71,8 +71,8 @@ class DownloadRequest private constructor(
// of them from a Parcel (from an Intent extra to submit a request to DownloadService) will fail.
//
// see: https://stackoverflow.com/a/22926342
dest.writeString(nonNullString(username))
dest.writeString(nonNullString(password))
dest.writeString(username ?: "")
dest.writeString(password ?: "")
dest.writeByte(if ((mediaEnqueued)) 1.toByte() else 0)
dest.writeBundle(arguments)
dest.writeByte(if (initiatedByUser) 1.toByte() else 0)
@ -183,10 +183,6 @@ class DownloadRequest private constructor(
companion object {
const val REQUEST_ARG_PAGE_NR: String = "page"
private fun nonNullString(str: String?): String {
return str ?: ""
}
private fun nullIfEmpty(str: String?): String? {
return if (str.isNullOrEmpty()) null else str
}

View File

@ -138,6 +138,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
downloadRequest.progressPercent = progressPercent
}
} catch (e: IOException) { Log.e(TAG, Log.getStackTraceString(e)) }
if (cancelled) onCancelled()
else {
// check if size specified in the response header is the same as the size of the

View File

@ -181,7 +181,7 @@ object Episodes {
@UnstableApi
fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job {
return runOnIOScope {
val removedFromQueue: MutableList<Episode> = ArrayList()
val removedFromQueue: MutableList<Episode> = mutableListOf()
val queueItems = curQueue.episodes.toMutableList()
for (episode in episodes) {
if (queueItems.remove(episode)) removedFromQueue.add(episode)

View File

@ -200,7 +200,7 @@ object Feeds {
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? {
Logd(TAG, "updateFeed called")
var resultFeed: Feed?
val unlistedItems: MutableList<Episode> = ArrayList()
// val unlistedItems: MutableList<Episode> = ArrayList()
// Look up feed in the feedslist
val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true)
@ -212,7 +212,8 @@ object Feeds {
addNewFeedsSync(context, newFeed)
// Update with default values that are set in database
resultFeed = searchFeedByIdentifyingValueOrID(newFeed)
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() }
// TODO: This doesn't appear needed as unlistedItems is still empty
// if (removeUnlistedItems && unlistedItems.isNotEmpty()) runBlocking { deleteEpisodes(context, unlistedItems).join() }
} catch (e: InterruptedException) { e.printStackTrace()
} catch (e: ExecutionException) { e.printStackTrace() }
return resultFeed
@ -226,7 +227,7 @@ object Feeds {
savedFeed.updateFromOther(newFeed)
}
} else {
Logd(TAG, "New feed has a higher page number.")
Logd(TAG, "New feed has a higher page number: ${newFeed.nextPageLink}")
savedFeed.nextPageLink = newFeed.nextPageLink
}
val priorMostRecent = savedFeed.mostRecentItem
@ -240,9 +241,9 @@ object Feeds {
// Look for new or updated Items
for (idx in newFeed.episodes.indices) {
val episode = newFeed.episodes[idx]
var oldItem = savedFeedAssistant.searchEpisodeByIdentifyingValue(episode)
var oldItem = savedFeedAssistant.getEpisodeByIdentifyingValue(episode)
if (!newFeed.isLocalFeed && oldItem == null) {
oldItem = savedFeedAssistant.searchEpisodeGuessDuplicate(episode)
oldItem = savedFeedAssistant.guessDuplicate(episode)
if (oldItem != null) {
Logd(TAG, "Repaired duplicate: $oldItem, $episode")
addDownloadStatus(DownloadResult(savedFeed.id,
@ -257,6 +258,7 @@ object Feeds {
${EpisodeAssistant.duplicateEpisodeDetails(episode)}
""".trimIndent()))
oldItem.identifier = episode.identifier
// queue for syncing with server
if (isProviderConnected && oldItem.isPlayed() && oldItem.media != null) {
val durs = oldItem.media!!.getDuration() / 1000
val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY)
@ -295,12 +297,13 @@ object Feeds {
}
savedFeedAssistant.clear()
val unlistedItems: MutableList<Episode> = ArrayList()
// identify episodes to be removed
if (removeUnlistedItems) {
val it = savedFeed.episodes.toMutableList().iterator()
while (it.hasNext()) {
val feedItem = it.next()
if (newFeedAssistant.searchEpisodeByIdentifyingValue(feedItem) == null) {
if (newFeedAssistant.getEpisodeByIdentifyingValue(feedItem) == null) {
unlistedItems.add(feedItem)
it.remove()
}
@ -315,7 +318,7 @@ object Feeds {
resultFeed = savedFeed
try {
upsertBlk(savedFeed) {}
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() }
if (removeUnlistedItems && unlistedItems.isNotEmpty()) runBlocking { deleteEpisodes(context, unlistedItems).join() }
} catch (e: InterruptedException) { e.printStackTrace()
} catch (e: ExecutionException) { e.printStackTrace() }
return resultFeed
@ -461,7 +464,7 @@ object Feeds {
}
// savedFeedId == 0L means saved feed
class FeedAssistant(val feed: Feed, val savedFeedId: Long = 0L) {
class FeedAssistant(val feed: Feed, private val savedFeedId: Long = 0L) {
val map = mutableMapOf<String, Episode>()
val tag: String = if (savedFeedId == 0L) "Saved feed" else "New feed"
@ -538,10 +541,10 @@ object Feeds {
${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)}
""".trimIndent()))
}
fun searchEpisodeByIdentifyingValue(item: Episode): Episode? {
fun getEpisodeByIdentifyingValue(item: Episode): Episode? {
return map[item.identifyingValue]
}
fun searchEpisodeGuessDuplicate(item: Episode): Episode? {
fun guessDuplicate(item: Episode): Episode? {
var episode = map[item.identifier]
if (episode != null) return episode
val url = item.media?.getStreamUrl()

View File

@ -84,6 +84,7 @@ class Feed : RealmObject {
var preferences: FeedPreferences? = null
// TODO: this might not be needed
var measures: FeedMeasures? = null
var hasVideoMedia: Boolean = false
@ -126,15 +127,6 @@ class Feed : RealmObject {
preferences?.sortOrderCode = value.code
}
// @Ignore
// var sortOrderAux: EpisodeSortOrder? = null
// get() = fromCode(preferences?.sortOrderAuxCode ?: 0)
// set(value) {
// if (value == null) return
// field = value
// preferences?.sortOrderAuxCode = value.code
// }
@Ignore
val mostRecentItem: Episode?
get() = realm.query(Episode::class).query("feedId == $id SORT(pubDate DESC)").first().find()

View File

@ -3,6 +3,7 @@ package ac.mdiq.podcini.storage.model
import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.annotations.Index
// TODO: this might not be needed
class FeedMeasures : EmbeddedRealmObject {
@Index
var feedID: Long = 0L

View File

@ -97,7 +97,7 @@ import java.util.concurrent.Semaphore
_binding = FeedItemListFragmentBinding.inflate(inflater)
_dialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
binding.toolbar.inflateMenu(R.menu.feedlist)
binding.toolbar.inflateMenu(R.menu.feed_episodes)
binding.toolbar.setOnMenuItemClickListener(this)
binding.toolbar.setOnLongClickListener {
binding.recyclerView.scrollToPosition(5)
@ -132,7 +132,7 @@ import java.util.concurrent.Semaphore
val iconTintManager: ToolbarIconTintManager = object : ToolbarIconTintManager(
requireContext(), binding.toolbar, binding.collapsingToolbar) {
override fun doTint(themedContext: Context) {
binding.toolbar.menu.findItem(R.id.refresh_item).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh))
binding.toolbar.menu.findItem(R.id.refresh_feed).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh))
binding.toolbar.menu.findItem(R.id.action_search).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search))
}
}
@ -268,7 +268,7 @@ import java.util.concurrent.Semaphore
binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(feed!!.link != null)
binding.toolbar.menu.findItem(R.id.refresh_complete_item).setVisible(feed!!.isPaged)
if (StringUtils.isBlank(feed!!.link)) binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(false)
if (feed!!.isLocalFeed) binding.toolbar.menu.findItem(R.id.share_item).setVisible(false)
if (feed!!.isLocalFeed) binding.toolbar.menu.findItem(R.id.share_feed).setVisible(false)
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -285,8 +285,8 @@ import java.util.concurrent.Semaphore
}
when (item.itemId) {
R.id.visit_website_item -> if (feed!!.link != null) IntentUtils.openInBrowser(requireContext(), feed!!.link!!)
R.id.share_item -> ShareUtils.shareFeedLink(requireContext(), feed!!)
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)
R.id.share_feed -> ShareUtils.shareFeedLink(requireContext(), feed!!)
R.id.refresh_feed -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)
R.id.refresh_complete_item -> {
Thread {
try {
@ -295,21 +295,14 @@ import java.util.concurrent.Semaphore
it.nextPageLink = it.downloadUrl
it.pageNr = 0
}
// val feed_ = unmanaged(feed!!)
// feed_.nextPageLink = feed_.downloadUrl
// feed_.pageNr = 0
// upsertBlk(feed_) {}
FeedUpdateManager.runOnce(requireContext(), feed_)
}
} catch (e: ExecutionException) {
throw RuntimeException(e)
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
} catch (e: ExecutionException) { throw RuntimeException(e)
} catch (e: InterruptedException) { throw RuntimeException(e) }
}.start()
}
R.id.sort_items -> SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog")
R.id.rename_item -> CustomFeedNameDialog(activity as Activity, feed!!).show()
R.id.rename_feed -> CustomFeedNameDialog(activity as Activity, feed!!).show()
R.id.remove_feed -> {
RemoveFeedDialog.show(requireContext(), feed!!) {
(activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)

View File

@ -6,11 +6,13 @@ import ac.mdiq.podcini.databinding.FeedinfoBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
import ac.mdiq.podcini.net.utils.HtmlToPlainText
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.database.Feeds.updateFeedDownloadURL
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.FeedFunding
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
import ac.mdiq.podcini.ui.statistics.FeedStatisticsFragment
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
import ac.mdiq.podcini.ui.utils.ToolbarIconTintManager
@ -248,6 +250,13 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}.show()
}
R.id.remove_feed -> {
RemoveFeedDialog.show(requireContext(), feed) {
(activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
// Make sure fragment is hidden before actually starting to delete
requireActivity().supportFragmentManager.executePendingTransactions()
}
}
else -> return false
}
return true
@ -255,10 +264,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@UnstableApi private fun addLocalFolderResult(uri: Uri?) {
if (uri == null) return
reconnectLocalFolder(uri)
}
@UnstableApi private fun reconnectLocalFolder(uri: Uri) {
// reconnectLocalFolder(uri)
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
@ -279,6 +285,27 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
// @UnstableApi private fun reconnectLocalFolder(uri: Uri) {
// lifecycleScope.launch {
// try {
// withContext(Dispatchers.IO) {
// requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
// val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
// requireNotNull(documentFile) { "Unable to retrieve document tree" }
// feed.downloadUrl = Feed.PREFIX_LOCAL_FOLDER + uri.toString()
// updateFeed(requireContext(), feed, true)
// }
// withContext(Dispatchers.Main) {
// (activity as MainActivity).showSnackbarAbovePlayer(string.ok, Snackbar.LENGTH_SHORT)
// }
// } catch (e: Throwable) {
// withContext(Dispatchers.Main) {
// (activity as MainActivity).showSnackbarAbovePlayer(e.localizedMessage, Snackbar.LENGTH_LONG)
// }
// }
// }
// }
private var eventSink: Job? = null
private fun cancelFlowEvents() {
eventSink?.cancel()
@ -323,11 +350,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
runBlocking { updateFeedDownloadURL(original, updated).join() }
feed.downloadUrl = updated
runOnce(activityRef.get()!!, feed)
} catch (e: ExecutionException) {
throw RuntimeException(e)
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
} catch (e: ExecutionException) { throw RuntimeException(e)
} catch (e: InterruptedException) { throw RuntimeException(e) }
}
@UnstableApi private fun showConfirmAlertDialog(url: String) {
val activity = activityRef.get()

View File

@ -5,9 +5,9 @@ import ac.mdiq.podcini.databinding.EditTextDialogBinding
import ac.mdiq.podcini.databinding.OnlineFeedviewFragmentBinding
import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.download.service.Downloader
import ac.mdiq.podcini.net.download.service.HttpDownloader
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
@ -24,8 +24,6 @@ import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment.Companion
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
import ac.mdiq.podcini.util.EventFlow
@ -38,7 +36,6 @@ import ac.mdiq.vista.extractor.channel.ChannelInfo
import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo
import ac.mdiq.vista.extractor.exceptions.ExtractionException
import ac.mdiq.vista.extractor.playlist.PlaylistInfo
import ac.mdiq.vista.extractor.stream.StreamInfo
import ac.mdiq.vista.extractor.stream.StreamInfoItem
import android.app.Dialog
import android.content.Context
@ -239,7 +236,10 @@ class OnlineFeedFragment : Fragment() {
private fun htmlOrXml(url: String): String? {
val connection = URL(url).openConnection() as HttpURLConnection
var type: String? = null
try { type = connection.contentType } catch (e: IOException) { Log.e(TAG, "Error connecting to URL", e) } finally { connection.disconnect() }
try { type = connection.contentType } catch (e: IOException) {
Log.e(TAG, "Error connecting to URL", e)
showErrorDialog(e.message, "")
} finally { connection.disconnect() }
if (type == null) return null
Logd(TAG, "connection type: $type")
return when {
@ -273,8 +273,7 @@ class OnlineFeedFragment : Fragment() {
var infoItems = playlistInfo.relatedItems
var nextPage = playlistInfo.nextPage
Logd(TAG, "infoItems: ${infoItems.size}")
var i = 0
while (infoItems.isNotEmpty() && i++ < 2) {
while (infoItems.isNotEmpty()) {
for (r in infoItems) {
Logd(TAG, "startFeedBuilding relatedItem: $r")
if (r.infoType != InfoItem.InfoType.STREAM) continue
@ -283,18 +282,27 @@ class OnlineFeedFragment : Fragment() {
e.feedId = feed_.id
eList.add(e)
}
if (nextPage == null) break
if (nextPage == null || eList.size > 500) break
try {
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
nextPage = page.nextPage
infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}")
} catch (e: Throwable) {
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
break
}
}
feed_.episodes = eList
withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
} else {
val channelInfo = ChannelInfo.getInfo(service, url)
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
if (channelInfo.tabs.isEmpty()) return@launch
if (channelInfo.tabs.isEmpty()) {
withContext(Dispatchers.Main) { showErrorDialog("Channel is empty", "") }
return@launch
}
try {
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
@ -306,8 +314,7 @@ class OnlineFeedFragment : Fragment() {
var infoItems = channelTabInfo.relatedItems
var nextPage = channelTabInfo.nextPage
Logd(TAG, "infoItems: ${infoItems.size}")
var i = 0
while (infoItems.isNotEmpty() && i++ < 2) {
while (infoItems.isNotEmpty()) {
for (r in infoItems) {
Logd(TAG, "startFeedBuilding relatedItem: $r")
if (r.infoType != InfoItem.InfoType.STREAM) continue
@ -316,17 +323,29 @@ class OnlineFeedFragment : Fragment() {
e.feedId = feed_.id
eList.add(e)
}
if (nextPage == null) break
if (nextPage == null || eList.size > 200) break
try {
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
nextPage = page.nextPage
infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}")
} catch (e: Throwable) {
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
break
}
}
feed_.episodes = eList
withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
} catch (e: Throwable) { Logd(TAG, "startFeedBuilding error1 ${e.message}") }
} catch (e: Throwable) {
Logd(TAG, "startFeedBuilding error1 ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
}
} catch (e: Throwable) {
Logd(TAG, "startFeedBuilding error ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
} catch (e: Throwable) { Logd(TAG, "startFeedBuilding error ${e.message}") }
}
return
}
@ -347,6 +366,7 @@ class OnlineFeedFragment : Fragment() {
"XML" -> {}
else -> {
Log.e(TAG, "unknown url type $urlType")
showErrorDialog("unknown url type $urlType", "")
return
}
}
@ -388,7 +408,10 @@ class OnlineFeedFragment : Fragment() {
}
}
}
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
}
}
@ -429,7 +452,10 @@ class OnlineFeedFragment : Fragment() {
this@OnlineFeedFragment.feeds = feeds
handleUpdatedFeedStatus()
}
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
}
}

View File

@ -16,7 +16,7 @@
custom:showAsAction="always">
</item>
<item
android:id="@+id/refresh_item"
android:id="@+id/refresh_feed"
android:menuCategory="container"
android:title="@string/refresh_label"
custom:showAsAction="never">
@ -44,13 +44,13 @@
</item>
<item
android:id="@+id/share_item"
android:id="@+id/share_feed"
android:menuCategory="container"
custom:showAsAction="never"
android:title="@string/share_label" />
<item
android:id="@+id/rename_item"
android:id="@+id/rename_feed"
android:menuCategory="container"
android:title="@string/rename_feed_label"
custom:showAsAction="never" />

View File

@ -21,4 +21,12 @@
<item
android:id="@+id/edit_feed_url_item"
android:title="@string/edit_url_menu" />
<item
android:id="@+id/remove_feed"
android:icon="@drawable/ic_delete"
android:menuCategory="container"
android:title="@string/remove_feed_label"
android:visible="true"
custom:showAsAction="collapseActionView">
</item>
</menu>

View File

@ -153,7 +153,7 @@
<string name="feed_auto_download_newer">Newest unplayed</string>
<string name="feed_auto_download_older">Oldest unplayed</string>
<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="feed_new_episodes_action_nothing">Nothing</string>
@ -363,7 +363,7 @@
<string name="queue_locked">Queue locked</string>
<string name="queue_unlocked">Queue unlocked</string>
<string name="queue_lock_warning">If you lock the queue, you can no longer swipe or reorder episodes.</string>
<string name="switch_queue">Switch queue</string>
<string name="switch_queue">Switch active queue</string>
<string name="open_queue">Open queue</string>
<string name="checkbox_do_not_show_again">Do not show again</string>
<string name="clear_queue_label">Clear queue</string>

View File

@ -1,3 +1,14 @@
# 6.7.2
* added menu item for removing feed in FeedInfo view
* menu item "Switch queue" is changed to "Switch active queue"
* Youtube and YT Music podcasts can be shared to Podcini
* initial max number of loaded items for Youtube and YT Music playlist and podcast is set 500
* initial max number of loaded items for Youtube channel is set 500
* added some error dialogs when handling shared links
* updated some dependencies including Compose
* compile and target SDK's are upped to 35
# 6.7.1
* ensured duplicate episodes are removed from secondary checking during refresh

View File

@ -0,0 +1,10 @@
Version 6.7.2:
* added menu item for removing feed in FeedInfo view
* menu item "Switch queue" is changed to "Switch active queue"
* Youtube and YT Music podcasts can be shared to Podcini
* initial max number of loaded items for Youtube and YT Music playlist and podcast is set 500
* initial max number of loaded items for Youtube channel is set 500
* added some error dialogs when handling shared links
* updated some dependencies including Compose
* compile and target SDK's are upped to 35