From efd51ed6e1d9bc860b62c58bb6686b84f9bdd41c Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Fri, 13 Sep 2024 23:34:06 +0100 Subject: [PATCH] 6.6.1 commit --- app/build.gradle | 4 +- .../playback/service/PlaybackService.kt | 3 +- .../podcini/preferences/UserPreferences.kt | 7 +++ .../ac/mdiq/podcini/storage/database/Feeds.kt | 29 +++++----- .../ui/activity/ShareReceiverActivity.kt | 53 +++++++++---------- .../ui/fragment/FeedEpisodesFragment.kt | 15 ++++-- .../ui/fragment/SubscriptionsFragment.kt | 23 +++++--- .../main/res/layout/feeditemlist_header.xml | 1 - .../res/layout/fragment_subscriptions.xml | 1 - app/src/main/res/values/strings.xml | 3 ++ app/src/main/res/xml/preferences_playback.xml | 13 +++-- changelog.md | 8 +++ .../android/en-US/changelogs/3020245.txt | 2 +- .../android/en-US/changelogs/3020246.txt | 7 +++ 14 files changed, 105 insertions(+), 64 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/3020246.txt diff --git a/app/build.gradle b/app/build.gradle index 7ff0e372..ea2d6f18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020245 - versionName "6.6.0" + versionCode 3020246 + versionName "6.6.1" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index 3a4f02db..bba628a9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -32,6 +32,7 @@ import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence +import ac.mdiq.podcini.preferences.UserPreferences.prefLowQualityMedia import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.storage.database.Episodes.addToHistory @@ -1432,7 +1433,7 @@ class PlaybackService : MediaLibraryService() { val streamInfo = StreamInfo.getInfo(vService, url) val audioStreamsList = getFilteredAudioStreams(streamInfo.audioStreams) Logd(TAG, "setDataSource1 audioStreamsList ${audioStreamsList.size}") - val audioIndex = if (isNetworkRestricted) 0 else audioStreamsList.size - 1 + val audioIndex = if (isNetworkRestricted && prefLowQualityMedia) 0 else audioStreamsList.size - 1 val audioStream = audioStreamsList[audioIndex] Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}") val aSource = DefaultMediaSourceFactory(context).createMediaSource( diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt index b8f13cd0..c82e7632 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -197,6 +197,12 @@ object UserPreferences { appPrefs.edit().putBoolean(Prefs.prefStreamOverDownload.name, stream).apply() } + var prefLowQualityMedia: Boolean + get() = appPrefs.getBoolean(Prefs.prefLowQualityOnMobile.name, false) + set(stream) { + appPrefs.edit().putBoolean(Prefs.prefLowQualityOnMobile.name, stream).apply() + } + /** * Sets up the UserPreferences class. * @throws IllegalArgumentException if context is null @@ -324,6 +330,7 @@ object UserPreferences { prefPauseForFocusLoss, prefPlaybackTimeRespectsSpeed, prefStreamOverDownload, + prefLowQualityOnMobile, prefSpeedforwardSpeed, // Network diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index 77efd6e9..d15736bc 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -1,6 +1,5 @@ package ac.mdiq.podcini.storage.database -import ac.mdiq.podcini.BuildConfig import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink @@ -20,18 +19,20 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT -import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName -import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent +import ac.mdiq.podcini.util.Logd import android.app.backup.BackupManager import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile import io.realm.kotlin.ext.asFlow -import io.realm.kotlin.notifications.* +import io.realm.kotlin.notifications.ResultsChange +import io.realm.kotlin.notifications.SingleQueryChange +import io.realm.kotlin.notifications.UpdatedObject +import io.realm.kotlin.notifications.UpdatedResults import kotlinx.coroutines.* import java.io.File import java.text.DateFormat @@ -241,7 +242,7 @@ object Feeds { """.trimIndent())) continue } - var oldItem = EpisodeAssistant.searchEpisodeByIdentifyingValue(savedFeed.episodes, episode) + var oldItem = searchEpisodeByIdentifyingValue(savedFeed.episodes, episode) if (!newFeed.isLocalFeed && oldItem == null) { oldItem = EpisodeAssistant.searchEpisodeGuessDuplicate(savedFeed.episodes, episode) if (oldItem != null) { @@ -299,7 +300,7 @@ object Feeds { val it = savedFeed.episodes.toMutableList().iterator() while (it.hasNext()) { val feedItem = it.next() - if (EpisodeAssistant.searchEpisodeByIdentifyingValue(newFeed.episodes, feedItem) == null) { + if (searchEpisodeByIdentifyingValue(newFeed.episodes, feedItem) == null) { unlistedItems.add(feedItem) it.remove() } @@ -442,15 +443,13 @@ object Feeds { if (searchEpisodeByIdentifyingValue(feed.episodes, episode) != null) return Logd(TAG, "addToYoutubeSyndicate adding new episode: ${episode.title}") - runOnIOScope { - episode.feed = feed - episode.id = Feed.newId() - episode.feedId = feed.id - episode.media?.id = episode.id - upsert(episode) {} - feed.episodes.add(episode) - upsert(feed) {} - } + episode.feed = feed + episode.id = Feed.newId() + episode.feedId = feed.id + episode.media?.id = episode.id + upsertBlk(episode) {} + feed.episodes.add(episode) + upsertBlk(feed) {} } /** diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt index 4932fb83..b8b571d1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt @@ -3,7 +3,6 @@ package ac.mdiq.podcini.ui.activity import ac.mdiq.podcini.R import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfo import ac.mdiq.podcini.storage.database.Feeds.addToYoutubeSyndicate -import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.util.Logd import ac.mdiq.vista.extractor.Vista @@ -13,7 +12,6 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.Log -import android.view.ViewGroup import androidx.activity.compose.setContent import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity @@ -23,7 +21,6 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -32,14 +29,14 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.net.URLDecoder class ShareReceiverActivity : AppCompatActivity() { + var feedUrl: String? = null + @OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - var feedUrl: String? = null when { intent.hasExtra(ARG_FEEDURL) -> feedUrl = intent.getStringExtra(ARG_FEEDURL) intent.action == Intent.ACTION_SEND -> feedUrl = intent.getStringExtra(Intent.EXTRA_TEXT) @@ -50,7 +47,7 @@ class ShareReceiverActivity : AppCompatActivity() { showNoPodcastFoundError() return } - if (!feedUrl.startsWith("http")) { + if (!feedUrl!!.startsWith("http")) { val uri = Uri.parse(feedUrl) val urlString = uri?.getQueryParameter("url") if (urlString != null) feedUrl = URLDecoder.decode(urlString, "UTF-8") @@ -59,32 +56,26 @@ class ShareReceiverActivity : AppCompatActivity() { when { // plain text feedUrl!!.matches(Regex("^[^\\s<>/]+\$")) -> { - val intent = MainActivity.showOnlineSearch(this, feedUrl) + val intent = MainActivity.showOnlineSearch(this, feedUrl!!) startActivity(intent) finish() } // Youtube media - feedUrl.startsWith("https://youtube.com/watch?") -> { + feedUrl!!.startsWith("https://youtube.com/watch?") -> { Logd(TAG, "got youtube media") - CoroutineScope(Dispatchers.IO).launch { - val info = StreamInfo.getInfo(Vista.getService(0), feedUrl) - Logd(TAG, "info: $info") - val episode = episodeFromStreamInfo(info) - Logd(TAG, "episode: $episode") - withContext(Dispatchers.Main) { - setContent { - val showDialog = remember { mutableStateOf(true) } - CustomTheme(this@ShareReceiverActivity) { - confirmAddEpisode(showDialog.value, episode, onDismissRequest = { showDialog.value = false }) - } - } + setContent { + val showDialog = remember { mutableStateOf(true) } + CustomTheme(this@ShareReceiverActivity) { + confirmAddEpisode(showDialog.value, onDismissRequest = { + showDialog.value = false + finish() + }) } } - } else -> { Logd(TAG, "Activity was started with url $feedUrl") - val intent = MainActivity.showOnlineFeed(this, feedUrl) + val intent = MainActivity.showOnlineFeed(this, feedUrl!!) // intent.putExtra(MainActivity.Extras.started_from_share.name, getIntent().getBooleanExtra(MainActivity.Extras.started_from_share.name, false)) startActivity(intent) finish() @@ -93,7 +84,7 @@ class ShareReceiverActivity : AppCompatActivity() { } @Composable - fun confirmAddEpisode(showDialog: Boolean, episode: Episode, onDismissRequest: () -> Unit) { + fun confirmAddEpisode(showDialog: Boolean, onDismissRequest: () -> Unit) { if (showDialog) { Dialog(onDismissRequest = { onDismissRequest() }) { Card( @@ -106,11 +97,11 @@ class ShareReceiverActivity : AppCompatActivity() { modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center ) { - var checked by remember { mutableStateOf(false) } + var audioOnly by remember { mutableStateOf(false) } Row(Modifier.fillMaxWidth()) { - Checkbox(checked = checked, + Checkbox(checked = audioOnly, onCheckedChange = { - checked = it + audioOnly = it } ) Text( @@ -119,8 +110,14 @@ class ShareReceiverActivity : AppCompatActivity() { ) } Button(onClick = { - addToYoutubeSyndicate(episode, !checked) - finish() + CoroutineScope(Dispatchers.IO).launch { + val info = StreamInfo.getInfo(Vista.getService(0), feedUrl!!) + Logd(TAG, "info: $info") + val episode = episodeFromStreamInfo(info) + Logd(TAG, "episode: $episode") + addToYoutubeSyndicate(episode, !audioOnly) + } + onDismissRequest() }) { Text("Confirm") } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index 47240324..84bb5d84 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -71,6 +71,8 @@ import java.util.concurrent.Semaphore private lateinit var swipeActions: SwipeActions private lateinit var nextPageLoader: MoreContentListFooterUtil + private var infoTextFiltered = "" + private var infoTextUpdate = "" private var displayUpArrow = false private var headerCreated = false private var feedID: Long = 0 @@ -155,6 +157,7 @@ import java.util.concurrent.Semaphore }) binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance)) + binding.swipeRefresh.setProgressViewEndTarget(false, 0) binding.swipeRefresh.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) } @@ -429,6 +432,8 @@ import java.util.concurrent.Semaphore private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdatingEvent) { nextPageLoader.setLoadingState(event.isRunning) if (!event.isRunning) nextPageLoader.root.visibility = View.GONE + infoTextUpdate = if (event.isRunning) getString(R.string.refreshing_label) else "" + binding.header.txtvInformation.text = ("{gmo-info} $infoTextFiltered $infoTextUpdate") binding.swipeRefresh.isRefreshing = event.isRunning } @@ -454,18 +459,20 @@ import java.util.concurrent.Semaphore binding.header.txtvTitle.text = feed!!.title binding.header.txtvAuthor.text = feed!!.author + binding.header.txtvInformation.setOnClickListener {} + infoTextFiltered = "" if (!feed?.preferences?.filterString.isNullOrEmpty()) { val filter: EpisodeFilter = feed!!.episodeFilter if (filter.values.isNotEmpty()) { - binding.header.txtvInformation.text = ("{gmo-info} " + this.getString(R.string.filtered_label)) + infoTextFiltered = this.getString(R.string.filtered_label) binding.header.txtvInformation.setOnClickListener { val dialog = FeedEpisodeFilterDialog(feed) dialog.filter = feed!!.episodeFilter dialog.show(childFragmentManager, null) } - binding.header.txtvInformation.visibility = View.VISIBLE - } else binding.header.txtvInformation.visibility = View.GONE - } else binding.header.txtvInformation.visibility = View.GONE + } + } + binding.header.txtvInformation.text = ("{gmo-info} $infoTextFiltered $infoTextUpdate") } @UnstableApi private fun setupHeaderView() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index e3a7bed6..5e8bb36d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -110,6 +110,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec private lateinit var catAdapter: ArrayAdapter + private var infoTextFiltered = "" + private var infoTextUpdate = "" private var tagFilterIndex = 1 // TODO: currently not used private var displayedFolder: String = "" @@ -187,7 +189,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } else false } - binding.progressBar.visibility = View.VISIBLE +// binding.progressBar.visibility = View.VISIBLE + binding.progressBar.visibility = View.GONE val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd subscriptionAddButton.setOnClickListener { @@ -218,8 +221,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec private fun setSwipeRefresh() { if (swipeToRefresh) { binding.swipeRefresh.isEnabled = true + binding.swipeRefresh.setProgressViewEndTarget(false, 0) binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance)) binding.swipeRefresh.setOnRefreshListener { + Logd(TAG, "running FeedUpdateManager") FeedUpdateManager.runOnceOrAsk(requireContext()) } } else binding.swipeRefresh.isEnabled = false @@ -327,7 +332,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec when (event) { is FlowEvent.FeedUpdatingEvent -> { Logd(TAG, "FeedUpdateRunningEvent: ${event.isRunning}") - binding.swipeRefresh.isRefreshing = event.isRunning + infoTextUpdate = if (event.isRunning) " " + this@SubscriptionsFragment.getString(R.string.refreshing_label) else "" + binding.txtvInformation.text = (infoTextFiltered + infoTextUpdate) + if (swipeToRefresh) binding.swipeRefresh.isRefreshing = event.isRunning if (!event.isRunning && event.id != prevFeedUpdatingEvent?.id) loadSubscriptions() prevFeedUpdatingEvent = event } @@ -377,18 +384,20 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec // We have fewer items. This can result in items being selected that are no longer visible. if (feedListFiltered.size > feedList.size) adapter.endSelectMode() filterOnTag() - binding.progressBar.visibility = View.GONE +// binding.progressBar.visibility = View.GONE adapter.setItems(feedListFiltered) binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() + infoTextFiltered = " " + binding.txtvInformation.setOnClickListener {} if (feedsFilter.isNotEmpty()) { val filter = FeedFilter(feedsFilter) - binding.txtvInformation.text = ("{gmo-info} " + getString(R.string.filtered_label)) + infoTextFiltered = getString(R.string.filtered_label) binding.txtvInformation.setOnClickListener { val dialog = FeedFilterDialog.newInstance(filter) dialog.show(childFragmentManager, null) } - binding.txtvInformation.visibility = View.VISIBLE - } else binding.txtvInformation.visibility = View.GONE + } + binding.txtvInformation.text = (infoTextFiltered + infoTextUpdate) emptyView.updateVisibility() } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) @@ -1187,7 +1196,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } } - fun onFilterChanged(newFilterValues: Set) { + private fun onFilterChanged(newFilterValues: Set) { feedsFilter = StringUtils.join(newFilterValues, ",") Logd(TAG, "onFilterChanged: $feedsFilter") EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues)) diff --git a/app/src/main/res/layout/feeditemlist_header.xml b/app/src/main/res/layout/feeditemlist_header.xml index e5041d99..4a579892 100644 --- a/app/src/main/res/layout/feeditemlist_header.xml +++ b/app/src/main/res/layout/feeditemlist_header.xml @@ -187,7 +187,6 @@ android:padding="2dp" android:background="?android:attr/colorBackground" android:foreground="?android:attr/selectableItemBackground" - android:visibility="gone" android:gravity="center" android:textColor="?attr/colorAccent" tools:visibility="visible" diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml index b454f6b3..ceac477d 100644 --- a/app/src/main/res/layout/fragment_subscriptions.xml +++ b/app/src/main/res/layout/fragment_subscriptions.xml @@ -50,7 +50,6 @@ android:padding="2dp" android:background="?android:attr/colorBackground" android:foreground="?android:attr/selectableItemBackground" - android:visibility="gone" android:gravity="center" android:textColor="?attr/colorAccent" tools:visibility="visible" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7609d6cb..81221ebd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,6 +119,7 @@ Error An error occurred: Refresh + Refreshing Reconsile Chapters No chapters @@ -478,6 +479,8 @@ Only play audio of a video media Prefer streaming Display stream button instead of download button in lists + Prefer low quality on mobile + On metered network, only low quality media (if available) is fetched Mobile updates Select what should be allowed over the mobile data connection Podcast refresh diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml index c757e03c..759cb1ce 100644 --- a/app/src/main/res/xml/preferences_playback.xml +++ b/app/src/main/res/xml/preferences_playback.xml @@ -57,10 +57,15 @@ android:summary="@string/pref_speed_forward_sum" android:title="@string/pref_speed_forward"/> + android:defaultValue="false" + android:key="prefStreamOverDownload" + android:summary="@string/pref_stream_over_download_sum" + android:title="@string/pref_stream_over_download_title"/> + Playback -> Playback control, and default it to false, + * if set true, Youtube media will use low quality audio on mobile network (which has been the default way of handling) + # 6.6.0 * added ability to receive shared Youtube media, diff --git a/fastlane/metadata/android/en-US/changelogs/3020245.txt b/fastlane/metadata/android/en-US/changelogs/3020245.txt index 362f1bf8..2c13986b 100644 --- a/fastlane/metadata/android/en-US/changelogs/3020245.txt +++ b/fastlane/metadata/android/en-US/changelogs/3020245.txt @@ -1,4 +1,4 @@ - Version 6.5.10: + Version 6.6.0: * added ability to receive shared Youtube media, * once received, the user can choose to set it as "audio only" before confirm diff --git a/fastlane/metadata/android/en-US/changelogs/3020246.txt b/fastlane/metadata/android/en-US/changelogs/3020246.txt new file mode 100644 index 00000000..28238e32 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020246.txt @@ -0,0 +1,7 @@ + Version 6.6.1: + +* the confirm dialog is more responsive when receiving a youtube media share +* receiving a youtube media share can be dismissed by not pressing the confirm button +* in Subscriptions and FeedEpisodes views, swipe down to refresh no longer blocks UI, only shows "Refreshing" status on the info bar +* added preference "Prefer low quality on mobile" under Settings -> Playback -> Playback control, and default it to false, + * if set true, Youtube media will use low quality audio on mobile network (which has been the default way of handling)