4.4.3 commit
This commit is contained in:
parent
d442601ba8
commit
24e7e671da
|
@ -149,8 +149,8 @@ android {
|
|||
// Version code schema (not used):
|
||||
// "1.2.3-beta4" -> 1020304
|
||||
// "1.2.3" -> 1020395
|
||||
versionCode 3020118
|
||||
versionName "4.4.2"
|
||||
versionCode 3020119
|
||||
versionName "4.4.3"
|
||||
|
||||
def commit = ""
|
||||
try {
|
||||
|
|
|
@ -16,7 +16,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
|||
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.storage.model.download.DownloadStatus
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.ui.appstartintent.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getDrawableFromAttr
|
||||
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
||||
|
@ -592,10 +591,9 @@ class MainActivity : CastEnabledActivity() {
|
|||
}
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
intent.hasExtra(EXTRA_EPISODES) -> {
|
||||
val episodes = (if (Build.VERSION.SDK_INT >= 33) intent.getSerializableExtra(EXTRA_EPISODES)
|
||||
else intent.getSerializableExtra(EXTRA_EPISODES)) as ArrayList<FeedItem>
|
||||
loadChildFragment(EpisodesListFragment.newInstance(episodes))
|
||||
intent.hasExtra(EXTRA_FEED_URL) -> {
|
||||
val feedurl = intent.getStringExtra(EXTRA_FEED_URL)
|
||||
if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl))
|
||||
}
|
||||
intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG) -> {
|
||||
val tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)
|
||||
|
@ -736,11 +734,11 @@ class MainActivity : CastEnabledActivity() {
|
|||
const val PREF_IS_FIRST_LAUNCH: String = "prefMainActivityIsFirstLaunch"
|
||||
|
||||
const val EXTRA_FEED_ID: String = "fragment_feed_id"
|
||||
const val EXTRA_FEED_URL: String = "fragment_feed_url"
|
||||
const val EXTRA_REFRESH_ON_START: String = "refresh_on_start"
|
||||
const val EXTRA_STARTED_FROM_SEARCH: String = "started_from_search"
|
||||
const val EXTRA_ADD_TO_BACK_STACK: String = "add_to_back_stack"
|
||||
const val KEY_GENERATED_VIEW_ID: String = "generated_view_id"
|
||||
const val EXTRA_EPISODES: String = "episodes_list"
|
||||
|
||||
@JvmStatic
|
||||
fun getIntentToOpenFeed(context: Context, feedId: Long): Intent {
|
||||
|
@ -750,12 +748,12 @@ class MainActivity : CastEnabledActivity() {
|
|||
return intent
|
||||
}
|
||||
|
||||
fun openEpisodesList(context: Context, episodes: ArrayList<FeedItem>): Intent {
|
||||
@JvmStatic
|
||||
fun showOnlineFeed(context: Context, feedUrl: String): Intent {
|
||||
val intent = Intent(context.applicationContext, MainActivity::class.java)
|
||||
intent.putExtra(EXTRA_EPISODES, episodes)
|
||||
intent.putExtra(EXTRA_FEED_URL, feedUrl)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
return intent
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,117 +1,24 @@
|
|||
package ac.mdiq.podcini.ui.activity
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.databinding.OnlinefeedviewActivityBinding
|
||||
import ac.mdiq.podcini.feed.FeedUrlNotFoundException
|
||||
import ac.mdiq.podcini.feed.parser.FeedHandler
|
||||
import ac.mdiq.podcini.feed.parser.FeedHandlerResult
|
||||
import ac.mdiq.podcini.feed.parser.UnsupportedFeedtypeException
|
||||
import ac.mdiq.podcini.net.common.UrlChecker.prepareUrl
|
||||
import ac.mdiq.podcini.net.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTranslucentTheme
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.service.download.DownloadRequestCreator.create
|
||||
import ac.mdiq.podcini.service.download.Downloader
|
||||
import ac.mdiq.podcini.service.download.HttpDownloader
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.storage.DBTasks
|
||||
import ac.mdiq.podcini.storage.DBWriter
|
||||
import ac.mdiq.podcini.storage.model.download.DownloadError
|
||||
import ac.mdiq.podcini.storage.model.download.DownloadResult
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
|
||||
import ac.mdiq.podcini.ui.glide.FastBlurTransformation
|
||||
import ac.mdiq.podcini.util.DownloadErrorLabel.from
|
||||
import ac.mdiq.podcini.util.event.EpisodeDownloadEvent
|
||||
import ac.mdiq.podcini.util.event.FeedListUpdateEvent
|
||||
import ac.mdiq.podcini.util.syndication.FeedDiscoverer
|
||||
import ac.mdiq.podcini.util.syndication.HtmlToPlainText
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.LightingColorFilter
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.observers.DisposableMaybeObserver
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.math.min
|
||||
import java.net.URLDecoder
|
||||
|
||||
/**
|
||||
* Downloads a feed from a feed URL and parses it. Subclasses can display the
|
||||
* feed object that was parsed. This activity MUST be started with a given URL
|
||||
* or an Exception will be thrown.
|
||||
*
|
||||
*
|
||||
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
||||
* and the activity will finish as soon as the error dialog is closed.
|
||||
*/
|
||||
// this now is only used for receiving shared feed url
|
||||
class OnlineFeedViewActivity : AppCompatActivity() {
|
||||
private var _binding: OnlinefeedviewActivityBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@Volatile
|
||||
private var feeds: List<Feed>? = null
|
||||
private var selectedDownloadUrl: String? = null
|
||||
private var downloader: Downloader? = null
|
||||
private var username: String? = null
|
||||
private var password: String? = null
|
||||
|
||||
private var isPaused = false
|
||||
private var didPressSubscribe = false
|
||||
private var isFeedFoundBySearch = false
|
||||
|
||||
private var dialog: Dialog? = null
|
||||
|
||||
private var download: Disposable? = null
|
||||
private var parser: Disposable? = null
|
||||
private var updater: Disposable? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTranslucentTheme(this))
|
||||
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
_binding = OnlinefeedviewActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.transparentBackground.setOnClickListener { finish() }
|
||||
binding.closeButton.setOnClickListener { finish() }
|
||||
binding.card.setOnClickListener(null)
|
||||
binding.card.setCardBackgroundColor(getColorFromAttr(this, R.attr.colorSurface))
|
||||
|
||||
var feedUrl: String? = null
|
||||
when {
|
||||
intent.hasExtra(ARG_FEEDURL) -> {
|
||||
|
@ -125,21 +32,22 @@ class OnlineFeedViewActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!feedUrl.isNullOrBlank() && !feedUrl.startsWith("http")) {
|
||||
val uri = Uri.parse(feedUrl)
|
||||
feedUrl = URLDecoder.decode(uri.getQueryParameter("url"), "UTF-8")
|
||||
}
|
||||
|
||||
if (feedUrl == null) {
|
||||
Log.e(TAG, "feedUrl is null.")
|
||||
showNoPodcastFoundError()
|
||||
} else {
|
||||
Log.d(TAG, "Activity was started with url $feedUrl")
|
||||
setLoadingLayout()
|
||||
// Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL
|
||||
if (feedUrl.contains("subscribeonandroid.com")) {
|
||||
feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "")
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
username = savedInstanceState.getString("username")
|
||||
password = savedInstanceState.getString("password")
|
||||
}
|
||||
lookupUrlAndDownload(feedUrl)
|
||||
|
||||
val intent = MainActivity.showOnlineFeed(this, feedUrl)
|
||||
intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH,
|
||||
getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false))
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,530 +65,15 @@ class OnlineFeedViewActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress indicator.
|
||||
*/
|
||||
private fun setLoadingLayout() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.feedDisplayContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isPaused = false
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
isPaused = true
|
||||
EventBus.getDefault().unregister(this)
|
||||
if (downloader != null && !downloader!!.isFinished) downloader!!.cancel()
|
||||
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
updater?.dispose()
|
||||
download?.dispose()
|
||||
parser?.dispose()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("username", username)
|
||||
outState.putString("password", password)
|
||||
}
|
||||
|
||||
private fun resetIntent(url: String) {
|
||||
val intent = Intent()
|
||||
intent.putExtra(ARG_FEEDURL, url)
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
val destIntent = Intent(this, MainActivity::class.java)
|
||||
if (NavUtils.shouldUpRecreateTask(this, destIntent)) {
|
||||
startActivity(destIntent)
|
||||
} else {
|
||||
NavUtils.navigateUpFromSameTask(this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun lookupUrlAndDownload(url: String) {
|
||||
download = PodcastSearcherRegistry.lookupUrl(url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe({ url1: String -> this.startFeedDownload(url1) },
|
||||
{ error: Throwable? ->
|
||||
if (error is FeedUrlNotFoundException) {
|
||||
tryToRetrieveFeedUrlBySearch(error)
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun tryToRetrieveFeedUrlBySearch(error: FeedUrlNotFoundException) {
|
||||
Log.d(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search")
|
||||
val url = searchFeedUrlByTrackName(error.trackName, error.artistName)
|
||||
if (url != null) {
|
||||
Log.d(TAG, "Successfully retrieve feed url")
|
||||
isFeedFoundBySearch = true
|
||||
startFeedDownload(url)
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
Log.d(TAG, "Failed to retrieve feed url")
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchFeedUrlByTrackName(trackName: String, artistName: String): String? {
|
||||
val searcher = CombinedSearcher()
|
||||
val query = "$trackName $artistName"
|
||||
val results = searcher.search(query).blockingGet()
|
||||
if (results.isNullOrEmpty()) return null
|
||||
for (result in results) {
|
||||
if (result?.feedUrl != null && result.author != null &&
|
||||
result.author.equals(artistName, ignoreCase = true) &&
|
||||
result.title.equals(trackName, ignoreCase = true)) {
|
||||
return result.feedUrl
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun startFeedDownload(url: String) {
|
||||
Log.d(TAG, "Starting feed download")
|
||||
selectedDownloadUrl = prepareUrl(url)
|
||||
val request = create(Feed(selectedDownloadUrl, null))
|
||||
.withAuthentication(username, password)
|
||||
.withInitiatedByUser(true)
|
||||
.build()
|
||||
|
||||
download = Observable.fromCallable {
|
||||
feeds = DBReader.getFeedList()
|
||||
downloader = HttpDownloader(request)
|
||||
downloader?.call()
|
||||
downloader?.result
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ status: DownloadResult? -> if (request.destination != null) checkDownloadResult(status, request.destination) },
|
||||
{ error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
}
|
||||
|
||||
private fun checkDownloadResult(status: DownloadResult?, destination: String) {
|
||||
if (status == null) return
|
||||
when {
|
||||
status.isSuccessful -> {
|
||||
parseFeed(destination)
|
||||
}
|
||||
status.reason == DownloadError.ERROR_UNAUTHORIZED -> {
|
||||
if (!isFinishing && !isPaused) {
|
||||
if (username != null && password != null) {
|
||||
Toast.makeText(this, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (downloader?.downloadRequest?.source != null) {
|
||||
dialog = FeedViewAuthenticationDialog(this@OnlineFeedViewActivity,
|
||||
R.string.authentication_notification_title, downloader!!.downloadRequest.source!!).create()
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
showErrorDialog(getString(from(status.reason)), status.reasonDetailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi @Subscribe
|
||||
fun onFeedListChanged(event: FeedListUpdateEvent?) {
|
||||
updater = Observable.fromCallable { DBReader.getFeedList() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ feeds: List<Feed>? ->
|
||||
this@OnlineFeedViewActivity.feeds = feeds
|
||||
handleUpdatedFeedStatus()
|
||||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }
|
||||
)
|
||||
}
|
||||
|
||||
@UnstableApi @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: EpisodeDownloadEvent?) {
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
|
||||
private fun parseFeed(destination: String) {
|
||||
Log.d(TAG, "Parsing feed")
|
||||
parser = Maybe.fromCallable { doParseFeed(destination) }
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeWith(object : DisposableMaybeObserver<FeedHandlerResult?>() {
|
||||
@UnstableApi override fun onSuccess(result: FeedHandlerResult) {
|
||||
showFeedInformation(result.feed, result.alternateFeedUrls)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// Ignore null result: We showed the discovery dialog.
|
||||
}
|
||||
|
||||
override fun onError(error: Throwable) {
|
||||
showErrorDialog(error.message, "")
|
||||
Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the feed.
|
||||
* @return The FeedHandlerResult if successful.
|
||||
* Null if unsuccessful but we started another attempt.
|
||||
* @throws Exception If unsuccessful but we do not know a resolution.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
private fun doParseFeed(destination: String): FeedHandlerResult? {
|
||||
val handler = FeedHandler()
|
||||
val feed = Feed(selectedDownloadUrl, null)
|
||||
feed.file_url = destination
|
||||
val destinationFile = File(destination)
|
||||
return try {
|
||||
handler.parseFeed(feed)
|
||||
} catch (e: UnsupportedFeedtypeException) {
|
||||
Log.d(TAG, "Unsupported feed type detected")
|
||||
if ("html".equals(e.rootElement, ignoreCase = true)) {
|
||||
if (selectedDownloadUrl != null) {
|
||||
val dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl!!)
|
||||
if (dialogShown) {
|
||||
null // Should not display an error message
|
||||
} else {
|
||||
throw UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html))
|
||||
}
|
||||
} else null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
throw e
|
||||
} finally {
|
||||
val rc = destinationFile.delete()
|
||||
Log.d(TAG, "Deleted feed source file. Result: $rc")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when feed parsed successfully.
|
||||
* This method is executed on the GUI thread.
|
||||
*/
|
||||
@UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map<String, String>) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.feedDisplayContainer.visibility = View.VISIBLE
|
||||
if (isFeedFoundBySearch) {
|
||||
val resId = R.string.no_feed_url_podcast_found_by_search
|
||||
Snackbar.make(binding.root, resId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
binding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
|
||||
|
||||
binding.episodeLabel.setOnClickListener { showEpisodes(feed.items)}
|
||||
|
||||
if (!feed.imageUrl.isNullOrBlank()) {
|
||||
Glide.with(this)
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.light_gray)
|
||||
.error(R.color.light_gray)
|
||||
.fitCenter()
|
||||
.dontAnimate())
|
||||
.into(binding.coverImage)
|
||||
Glide.with(this)
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.image_readability_tint)
|
||||
.error(R.color.image_readability_tint)
|
||||
.transform(FastBlurTransformation())
|
||||
.dontAnimate())
|
||||
.into(binding.backgroundImage)
|
||||
}
|
||||
|
||||
binding.titleLabel.text = feed.title
|
||||
binding.authorLabel.text = feed.author
|
||||
|
||||
binding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"")
|
||||
|
||||
binding.subscribeButton.setOnClickListener {
|
||||
if (feedInFeedlist()) {
|
||||
openFeed()
|
||||
} else {
|
||||
DBTasks.updateFeed(this, feed, false)
|
||||
didPressSubscribe = true
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnableAutodownload) {
|
||||
val preferences = getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
binding.autoDownloadCheckBox.isChecked = preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
||||
}
|
||||
|
||||
if (alternateFeedUrls.isEmpty()) {
|
||||
binding.alternateUrlsSpinner.visibility = View.GONE
|
||||
} else {
|
||||
binding.alternateUrlsSpinner.visibility = View.VISIBLE
|
||||
|
||||
val alternateUrlsList: MutableList<String> = ArrayList()
|
||||
val alternateUrlsTitleList: MutableList<String?> = ArrayList()
|
||||
|
||||
if (feed.download_url != null) alternateUrlsList.add(feed.download_url!!)
|
||||
alternateUrlsTitleList.add(feed.title)
|
||||
|
||||
alternateUrlsList.addAll(alternateFeedUrls.keys)
|
||||
for (url in alternateFeedUrls.keys) {
|
||||
alternateUrlsTitleList.add(alternateFeedUrls[url])
|
||||
}
|
||||
|
||||
val adapter: ArrayAdapter<String> = object : ArrayAdapter<String>(this,
|
||||
R.layout.alternate_urls_item, alternateUrlsTitleList) {
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
// reusing the old view causes a visual bug on Android <= 10
|
||||
return super.getDropDownView(position, null, parent)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item)
|
||||
binding.alternateUrlsSpinner.adapter = adapter
|
||||
binding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
selectedDownloadUrl = alternateUrlsList[position]
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
|
||||
@UnstableApi private fun openFeed() {
|
||||
// feed.getId() is always 0, we have to retrieve the id from the feed list from
|
||||
// the database
|
||||
val intent = MainActivity.getIntentToOpenFeed(this, feedId)
|
||||
intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH,
|
||||
getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false))
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@UnstableApi private fun showEpisodes(episodes: List<FeedItem>) {
|
||||
Log.d(TAG, "showEpisodes ${episodes.size}")
|
||||
if (episodes.isNullOrEmpty()) return
|
||||
val intent = MainActivity.openEpisodesList(this, ArrayList(episodes.subList(0, min(50, episodes.size-1))))
|
||||
intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH,
|
||||
getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false))
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@UnstableApi private fun handleUpdatedFeedStatus() {
|
||||
val dli = DownloadServiceInterface.get()
|
||||
if (dli == null || selectedDownloadUrl == null) return
|
||||
|
||||
when {
|
||||
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
|
||||
binding.subscribeButton.isEnabled = false
|
||||
binding.subscribeButton.setText(R.string.subscribing_label)
|
||||
}
|
||||
feedInFeedlist() -> {
|
||||
binding.subscribeButton.isEnabled = true
|
||||
binding.subscribeButton.setText(R.string.open)
|
||||
if (didPressSubscribe) {
|
||||
didPressSubscribe = false
|
||||
|
||||
val feed1 = DBReader.getFeed(feedId)?: return
|
||||
val feedPreferences = feed1.preferences
|
||||
if (feedPreferences != null) {
|
||||
if (isEnableAutodownload) {
|
||||
val autoDownload = binding.autoDownloadCheckBox.isChecked
|
||||
feedPreferences.autoDownload = autoDownload
|
||||
|
||||
val preferences = getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
val editor = preferences.edit()
|
||||
editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload)
|
||||
editor.apply()
|
||||
}
|
||||
if (username != null) {
|
||||
feedPreferences.username = username
|
||||
feedPreferences.password = password
|
||||
}
|
||||
DBWriter.setFeedPreferences(feedPreferences)
|
||||
}
|
||||
openFeed()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
binding.subscribeButton.isEnabled = true
|
||||
binding.subscribeButton.setText(R.string.subscribe_label)
|
||||
if (isEnableAutodownload) {
|
||||
binding.autoDownloadCheckBox.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun feedInFeedlist(): Boolean {
|
||||
return feedId != 0L
|
||||
}
|
||||
|
||||
private val feedId: Long
|
||||
get() {
|
||||
if (feeds == null) return 0
|
||||
|
||||
for (f in feeds!!) {
|
||||
if (f.download_url == selectedDownloadUrl) {
|
||||
return f.id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun showErrorDialog(errorMsg: String?, details: String) {
|
||||
if (!isFinishing && !isPaused) {
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(R.string.error_label)
|
||||
if (errorMsg != null) {
|
||||
val total = """
|
||||
$errorMsg
|
||||
|
||||
$details
|
||||
""".trimIndent()
|
||||
val errorMessage = SpannableString(total)
|
||||
errorMessage.setSpan(ForegroundColorSpan(-0x77777778),
|
||||
errorMsg.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
builder.setMessage(errorMessage)
|
||||
} else {
|
||||
builder.setMessage(R.string.download_error_error_unknown)
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.cancel() }
|
||||
if (intent.getBooleanExtra(ARG_WAS_MANUAL_URL, false)) {
|
||||
builder.setNeutralButton(R.string.edit_url_menu) { _: DialogInterface?, _: Int -> editUrl() }
|
||||
}
|
||||
builder.setOnCancelListener {
|
||||
setResult(RESULT_ERROR)
|
||||
finish()
|
||||
}
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
dialog = builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun editUrl() {
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(R.string.edit_url_menu)
|
||||
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
|
||||
if (downloader != null) {
|
||||
dialogBinding.urlEditText.setText(downloader!!.downloadRequest.source)
|
||||
}
|
||||
builder.setView(dialogBinding.root)
|
||||
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||
setLoadingLayout()
|
||||
lookupUrlAndDownload(dialogBinding.urlEditText.text.toString())
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, _: Int -> dialog1.cancel() }
|
||||
builder.setOnCancelListener {
|
||||
setResult(RESULT_ERROR)
|
||||
finish()
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found).
|
||||
*/
|
||||
private fun showFeedDiscoveryDialog(feedFile: File, baseUrl: String): Boolean {
|
||||
val fd = FeedDiscoverer()
|
||||
val urlsMap: Map<String, String>
|
||||
try {
|
||||
urlsMap = fd.findLinks(feedFile, baseUrl)
|
||||
if (urlsMap.isEmpty()) return false
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
if (isPaused || isFinishing) return false
|
||||
|
||||
val titles: MutableList<String?> = ArrayList()
|
||||
|
||||
val urls: List<String> = ArrayList(urlsMap.keys)
|
||||
for (url in urls) {
|
||||
titles.add(urlsMap[url])
|
||||
}
|
||||
|
||||
if (urls.size == 1) {
|
||||
// Skip dialog and display the item directly
|
||||
resetIntent(urls[0])
|
||||
startFeedDownload(urls[0])
|
||||
return true
|
||||
}
|
||||
|
||||
val adapter = ArrayAdapter(this@OnlineFeedViewActivity,
|
||||
R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles)
|
||||
val onClickListener = DialogInterface.OnClickListener { dialog: DialogInterface, which: Int ->
|
||||
val selectedUrl = urls[which]
|
||||
dialog.dismiss()
|
||||
resetIntent(selectedUrl)
|
||||
startFeedDownload(selectedUrl)
|
||||
}
|
||||
|
||||
val ab = MaterialAlertDialogBuilder(this@OnlineFeedViewActivity)
|
||||
.setTitle(R.string.feeds_label)
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener { _: DialogInterface? -> finish() }
|
||||
.setAdapter(adapter, onClickListener)
|
||||
|
||||
runOnUiThread {
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
dialog = ab.show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class FeedViewAuthenticationDialog(context: Context, titleRes: Int, private val feedUrl: String) :
|
||||
AuthenticationDialog(context, titleRes, true, username, password) {
|
||||
override fun onCancelled() {
|
||||
super.onCancelled()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onConfirmed(username: String, password: String) {
|
||||
this@OnlineFeedViewActivity.username = username
|
||||
this@OnlineFeedViewActivity.password = password
|
||||
startFeedDownload(feedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||
const val ARG_WAS_MANUAL_URL: String = "manual_url"
|
||||
private const val RESULT_ERROR = 2
|
||||
private const val TAG = "OnlineFeedViewActivity"
|
||||
private const val PREFS = "OnlineFeedViewActivityPreferences"
|
||||
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
|
||||
private const val DESCRIPTION_MAX_LINES_COLLAPSED = 20
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTranslucentTheme
|
||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||
|
||||
// This is for widget
|
||||
class PlaybackSpeedDialogActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTranslucentTheme(this))
|
||||
|
|
|
@ -33,15 +33,16 @@ import io.reactivex.android.schedulers.AndroidSchedulers
|
|||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
// TODO: need to enable
|
||||
class SelectSubscriptionActivity : AppCompatActivity() {
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
@Volatile
|
||||
private var listItems: List<Feed>? = null
|
||||
|
||||
private var _binding: SubscriptionSelectionActivityBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
@Volatile
|
||||
private var listItems: List<Feed> = listOf()
|
||||
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(ThemeSwitcher.getTranslucentTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -64,7 +65,7 @@ class SelectSubscriptionActivity : AppCompatActivity() {
|
|||
}
|
||||
binding.shortcutBtn.setOnClickListener {
|
||||
if (checkedPosition[0] != null && Intent.ACTION_CREATE_SHORTCUT == intent.action) {
|
||||
getBitmapFromUrl(listItems!![checkedPosition[0]!!])
|
||||
getBitmapFromUrl(listItems[checkedPosition[0]!!])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,10 +73,10 @@ class SelectSubscriptionActivity : AppCompatActivity() {
|
|||
fun getFeedItems(items: List<NavDrawerData.FeedDrawerItem?>, result: MutableList<Feed>): List<Feed> {
|
||||
for (item in items) {
|
||||
if (item == null) continue
|
||||
val feed: Feed = item.feed
|
||||
if (!result.contains(feed)) {
|
||||
result.add(feed)
|
||||
}
|
||||
val feed: Feed = item.feed
|
||||
if (!result.contains(feed)) {
|
||||
result.add(feed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import ac.mdiq.podcini.util.PlaybackStatus.isCurrentlyPlaying
|
|||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
|
||||
import ac.mdiq.podcini.storage.model.playback.RemoteMedia
|
||||
|
||||
abstract class ItemActionButton internal constructor(@JvmField var item: FeedItem) {
|
||||
abstract fun getLabel(): Int
|
||||
|
@ -38,9 +39,6 @@ abstract class ItemActionButton internal constructor(@JvmField var item: FeedIte
|
|||
isCurrentlyPlaying(media) -> {
|
||||
PauseActionButton(item)
|
||||
}
|
||||
item.feed == null -> {
|
||||
StreamActionButton(item)
|
||||
}
|
||||
item.feed != null && item.feed!!.isLocalFeed -> {
|
||||
PlayLocalActionButton(item)
|
||||
}
|
||||
|
@ -50,7 +48,7 @@ abstract class ItemActionButton internal constructor(@JvmField var item: FeedIte
|
|||
isDownloadingMedia -> {
|
||||
CancelDownloadActionButton(item)
|
||||
}
|
||||
isStreamOverDownload || item.feed == null -> {
|
||||
isStreamOverDownload || item.feed == null || item.feedId == 0L -> {
|
||||
StreamActionButton(item)
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.AddfeedBinding
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.net.discovery.*
|
||||
import ac.mdiq.podcini.net.download.FeedUpdateManager
|
||||
import ac.mdiq.podcini.storage.DBTasks
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.SortOrder
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
|
@ -17,16 +26,6 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.activity.OnlineFeedViewActivity
|
||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||
import ac.mdiq.podcini.storage.DBTasks
|
||||
import ac.mdiq.podcini.net.download.FeedUpdateManager
|
||||
import ac.mdiq.podcini.databinding.AddfeedBinding
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.SortOrder
|
||||
import ac.mdiq.podcini.net.discovery.*
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
@ -135,10 +134,8 @@ class AddFeedFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun addUrl(url: String) {
|
||||
val intent = Intent(getActivity(), OnlineFeedViewActivity::class.java)
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, url)
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_WAS_MANUAL_URL, true)
|
||||
startActivity(intent)
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(url)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
private fun performSearch() {
|
||||
|
|
|
@ -7,12 +7,11 @@ import ac.mdiq.podcini.databinding.SelectCountryDialogBinding
|
|||
import ac.mdiq.podcini.net.discovery.ItunesTopListLoader
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.ui.activity.OnlineFeedViewActivity
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
|
||||
import ac.mdiq.podcini.util.event.DiscoveryDefaultUpdateEvent
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
@ -23,8 +22,10 @@ import android.view.View.OnFocusChangeListener
|
|||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
|
@ -95,7 +96,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
// Inflate the layout for this fragment
|
||||
_binding = FragmentItunesSearchBinding.inflate(inflater)
|
||||
// val root = inflater.inflate(R.layout.fragment_itunes_search, container, false)
|
||||
|
@ -119,9 +120,8 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (podcast.feedUrl == null) {
|
||||
return@OnItemClickListener
|
||||
}
|
||||
val intent = Intent(activity, OnlineFeedViewActivity::class.java)
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl)
|
||||
startActivity(intent)
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
progressBar = binding.progressBar
|
||||
|
|
|
@ -234,7 +234,7 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
|||
val duration: Int = converter.convert(event.duration)
|
||||
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt())
|
||||
if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) {
|
||||
Log.w(AudioPlayerFragment.TAG, "Could not react to position observer update because of invalid time")
|
||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,646 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.databinding.OnlineFeedviewFragmentBinding
|
||||
import ac.mdiq.podcini.feed.FeedUrlNotFoundException
|
||||
import ac.mdiq.podcini.feed.parser.FeedHandler
|
||||
import ac.mdiq.podcini.feed.parser.FeedHandlerResult
|
||||
import ac.mdiq.podcini.feed.parser.UnsupportedFeedtypeException
|
||||
import ac.mdiq.podcini.net.common.UrlChecker.prepareUrl
|
||||
import ac.mdiq.podcini.net.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.service.download.DownloadRequestCreator.create
|
||||
import ac.mdiq.podcini.service.download.Downloader
|
||||
import ac.mdiq.podcini.service.download.HttpDownloader
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.storage.DBTasks
|
||||
import ac.mdiq.podcini.storage.DBWriter
|
||||
import ac.mdiq.podcini.storage.model.download.DownloadError
|
||||
import ac.mdiq.podcini.storage.model.download.DownloadResult
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
|
||||
import ac.mdiq.podcini.ui.glide.FastBlurTransformation
|
||||
import ac.mdiq.podcini.util.DownloadErrorLabel.from
|
||||
import ac.mdiq.podcini.util.event.EpisodeDownloadEvent
|
||||
import ac.mdiq.podcini.util.event.FeedListUpdateEvent
|
||||
import ac.mdiq.podcini.util.syndication.FeedDiscoverer
|
||||
import ac.mdiq.podcini.util.syndication.HtmlToPlainText
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.LightingColorFilter
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.observers.DisposableMaybeObserver
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* Downloads a feed from a feed URL and parses it. Subclasses can display the
|
||||
* feed object that was parsed. This activity MUST be started with a given URL
|
||||
* or an Exception will be thrown.
|
||||
*
|
||||
*
|
||||
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
||||
* and the activity will finish as soon as the error dialog is closed.
|
||||
*/
|
||||
class OnlineFeedViewFragment : Fragment() {
|
||||
private var _binding: OnlineFeedviewFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var displayUpArrow = false
|
||||
|
||||
@Volatile
|
||||
private var feeds: List<Feed>? = null
|
||||
private var selectedDownloadUrl: String? = null
|
||||
private var downloader: Downloader? = null
|
||||
private var username: String? = null
|
||||
private var password: String? = null
|
||||
|
||||
private var isPaused = false
|
||||
private var didPressSubscribe = false
|
||||
private var isFeedFoundBySearch = false
|
||||
|
||||
private var dialog: Dialog? = null
|
||||
|
||||
private var download: Disposable? = null
|
||||
private var parser: Disposable? = null
|
||||
private var updater: Disposable? = null
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = OnlineFeedviewFragmentBinding.inflate(layoutInflater)
|
||||
binding.closeButton.visibility = View.INVISIBLE
|
||||
binding.card.setOnClickListener(null)
|
||||
binding.card.setCardBackgroundColor(getColorFromAttr(requireContext(), R.attr.colorSurface))
|
||||
|
||||
Log.d(TAG, "fragment onCreateView")
|
||||
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) {
|
||||
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
}
|
||||
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||
|
||||
var feedUrl = requireArguments().getString(ARG_FEEDURL)
|
||||
|
||||
if (feedUrl == null) {
|
||||
Log.e(TAG, "feedUrl is null.")
|
||||
showNoPodcastFoundError()
|
||||
} else {
|
||||
Log.d(TAG, "Activity was started with url $feedUrl")
|
||||
setLoadingLayout()
|
||||
// Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL
|
||||
if (feedUrl.contains("subscribeonandroid.com")) {
|
||||
feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "")
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
username = savedInstanceState.getString("username")
|
||||
password = savedInstanceState.getString("password")
|
||||
}
|
||||
lookupUrlAndDownload(feedUrl)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun showNoPodcastFoundError() {
|
||||
requireActivity().runOnUiThread {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||
.setTitle(R.string.error_label)
|
||||
.setMessage(R.string.null_value_podcast_error)
|
||||
.setOnDismissListener {}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress indicator.
|
||||
*/
|
||||
private fun setLoadingLayout() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.feedDisplayContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isPaused = false
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
isPaused = true
|
||||
EventBus.getDefault().unregister(this)
|
||||
if (downloader != null && !downloader!!.isFinished) downloader!!.cancel()
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
updater?.dispose()
|
||||
download?.dispose()
|
||||
parser?.dispose()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_UP_ARROW, displayUpArrow)
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("username", username)
|
||||
outState.putString("password", password)
|
||||
}
|
||||
|
||||
private fun lookupUrlAndDownload(url: String) {
|
||||
download = PodcastSearcherRegistry.lookupUrl(url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe({ url1: String -> this.startFeedDownload(url1) },
|
||||
{ error: Throwable? ->
|
||||
if (error is FeedUrlNotFoundException) {
|
||||
tryToRetrieveFeedUrlBySearch(error)
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun tryToRetrieveFeedUrlBySearch(error: FeedUrlNotFoundException) {
|
||||
Log.d(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search")
|
||||
val url = searchFeedUrlByTrackName(error.trackName, error.artistName)
|
||||
if (url != null) {
|
||||
Log.d(TAG, "Successfully retrieve feed url")
|
||||
isFeedFoundBySearch = true
|
||||
startFeedDownload(url)
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
Log.d(TAG, "Failed to retrieve feed url")
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchFeedUrlByTrackName(trackName: String, artistName: String): String? {
|
||||
val searcher = CombinedSearcher()
|
||||
val query = "$trackName $artistName"
|
||||
val results = searcher.search(query).blockingGet()
|
||||
if (results.isNullOrEmpty()) return null
|
||||
for (result in results) {
|
||||
if (result?.feedUrl != null && result.author != null &&
|
||||
result.author.equals(artistName, ignoreCase = true) &&
|
||||
result.title.equals(trackName, ignoreCase = true)) {
|
||||
return result.feedUrl
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun startFeedDownload(url: String) {
|
||||
Log.d(TAG, "Starting feed download")
|
||||
selectedDownloadUrl = prepareUrl(url)
|
||||
val request = create(Feed(selectedDownloadUrl, null))
|
||||
.withAuthentication(username, password)
|
||||
.withInitiatedByUser(true)
|
||||
.build()
|
||||
|
||||
download = Observable.fromCallable {
|
||||
feeds = DBReader.getFeedList()
|
||||
downloader = HttpDownloader(request)
|
||||
downloader?.call()
|
||||
downloader?.result
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ status: DownloadResult? -> if (request.destination != null) checkDownloadResult(status, request.destination) },
|
||||
{ error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
}
|
||||
|
||||
private fun checkDownloadResult(status: DownloadResult?, destination: String) {
|
||||
if (status == null) return
|
||||
when {
|
||||
status.isSuccessful -> {
|
||||
parseFeed(destination)
|
||||
}
|
||||
status.reason == DownloadError.ERROR_UNAUTHORIZED -> {
|
||||
if (!isRemoving && !isPaused) {
|
||||
if (username != null && password != null) {
|
||||
Toast.makeText(requireContext(), R.string.download_error_unauthorized, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (downloader?.downloadRequest?.source != null) {
|
||||
dialog = FeedViewAuthenticationDialog(requireContext(),
|
||||
R.string.authentication_notification_title, downloader!!.downloadRequest.source!!).create()
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
showErrorDialog(getString(from(status.reason)), status.reasonDetailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi @Subscribe
|
||||
fun onFeedListChanged(event: FeedListUpdateEvent?) {
|
||||
updater = Observable.fromCallable { DBReader.getFeedList() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ feeds: List<Feed>? ->
|
||||
this@OnlineFeedViewFragment.feeds = feeds
|
||||
handleUpdatedFeedStatus()
|
||||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }
|
||||
)
|
||||
}
|
||||
|
||||
@UnstableApi @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: EpisodeDownloadEvent?) {
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
|
||||
private fun parseFeed(destination: String) {
|
||||
Log.d(TAG, "Parsing feed")
|
||||
parser = Maybe.fromCallable { doParseFeed(destination) }
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeWith(object : DisposableMaybeObserver<FeedHandlerResult?>() {
|
||||
@UnstableApi override fun onSuccess(result: FeedHandlerResult) {
|
||||
showFeedInformation(result.feed, result.alternateFeedUrls)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// Ignore null result: We showed the discovery dialog.
|
||||
}
|
||||
|
||||
override fun onError(error: Throwable) {
|
||||
showErrorDialog(error.message, "")
|
||||
Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the feed.
|
||||
* @return The FeedHandlerResult if successful.
|
||||
* Null if unsuccessful but we started another attempt.
|
||||
* @throws Exception If unsuccessful but we do not know a resolution.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
private fun doParseFeed(destination: String): FeedHandlerResult? {
|
||||
val handler = FeedHandler()
|
||||
val feed = Feed(selectedDownloadUrl, null)
|
||||
feed.file_url = destination
|
||||
val destinationFile = File(destination)
|
||||
return try {
|
||||
handler.parseFeed(feed)
|
||||
} catch (e: UnsupportedFeedtypeException) {
|
||||
Log.d(TAG, "Unsupported feed type detected")
|
||||
if ("html".equals(e.rootElement, ignoreCase = true)) {
|
||||
if (selectedDownloadUrl != null) {
|
||||
val dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl!!)
|
||||
if (dialogShown) {
|
||||
null // Should not display an error message
|
||||
} else {
|
||||
throw UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html))
|
||||
}
|
||||
} else null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
throw e
|
||||
} finally {
|
||||
val rc = destinationFile.delete()
|
||||
Log.d(TAG, "Deleted feed source file. Result: $rc")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when feed parsed successfully.
|
||||
* This method is executed on the GUI thread.
|
||||
*/
|
||||
@UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map<String, String>) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.feedDisplayContainer.visibility = View.VISIBLE
|
||||
if (isFeedFoundBySearch) {
|
||||
val resId = R.string.no_feed_url_podcast_found_by_search
|
||||
Snackbar.make(binding.root, resId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
binding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
|
||||
binding.episodeLabel.setOnClickListener { showEpisodes(feed.items)}
|
||||
|
||||
if (!feed.imageUrl.isNullOrBlank()) {
|
||||
Glide.with(this)
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.light_gray)
|
||||
.error(R.color.light_gray)
|
||||
.fitCenter()
|
||||
.dontAnimate())
|
||||
.into(binding.coverImage)
|
||||
Glide.with(this)
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.image_readability_tint)
|
||||
.error(R.color.image_readability_tint)
|
||||
.transform(FastBlurTransformation())
|
||||
.dontAnimate())
|
||||
.into(binding.backgroundImage)
|
||||
}
|
||||
|
||||
binding.titleLabel.text = feed.title
|
||||
binding.authorLabel.text = feed.author
|
||||
|
||||
binding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"")
|
||||
|
||||
binding.subscribeButton.setOnClickListener {
|
||||
if (feedInFeedlist()) {
|
||||
openFeed()
|
||||
} else {
|
||||
DBTasks.updateFeed(requireContext(), feed, false)
|
||||
didPressSubscribe = true
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnableAutodownload) {
|
||||
val preferences = requireContext().getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
binding.autoDownloadCheckBox.isChecked = preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
||||
}
|
||||
|
||||
if (alternateFeedUrls.isEmpty()) {
|
||||
binding.alternateUrlsSpinner.visibility = View.GONE
|
||||
} else {
|
||||
binding.alternateUrlsSpinner.visibility = View.VISIBLE
|
||||
|
||||
val alternateUrlsList: MutableList<String> = ArrayList()
|
||||
val alternateUrlsTitleList: MutableList<String?> = ArrayList()
|
||||
|
||||
if (feed.download_url != null) alternateUrlsList.add(feed.download_url!!)
|
||||
alternateUrlsTitleList.add(feed.title)
|
||||
|
||||
alternateUrlsList.addAll(alternateFeedUrls.keys)
|
||||
for (url in alternateFeedUrls.keys) {
|
||||
alternateUrlsTitleList.add(alternateFeedUrls[url])
|
||||
}
|
||||
|
||||
val adapter: ArrayAdapter<String> = object : ArrayAdapter<String>(requireContext(),
|
||||
R.layout.alternate_urls_item, alternateUrlsTitleList) {
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
// reusing the old view causes a visual bug on Android <= 10
|
||||
return super.getDropDownView(position, null, parent)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item)
|
||||
binding.alternateUrlsSpinner.adapter = adapter
|
||||
binding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
selectedDownloadUrl = alternateUrlsList[position]
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
|
||||
@UnstableApi private fun openFeed() {
|
||||
// feed.getId() is always 0, we have to retrieve the id from the feed list from
|
||||
// the database
|
||||
(activity as MainActivity).loadFeedFragmentById(feedId, null)
|
||||
}
|
||||
|
||||
@UnstableApi private fun showEpisodes(episodes: List<FeedItem>) {
|
||||
Log.d(TAG, "showEpisodes ${episodes.size}")
|
||||
if (episodes.isNullOrEmpty()) return
|
||||
val fragment: Fragment = EpisodesListFragment.newInstance(ArrayList(episodes))
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
@UnstableApi private fun handleUpdatedFeedStatus() {
|
||||
val dli = DownloadServiceInterface.get()
|
||||
if (dli == null || selectedDownloadUrl == null) return
|
||||
|
||||
when {
|
||||
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
|
||||
binding.subscribeButton.isEnabled = false
|
||||
binding.subscribeButton.setText(R.string.subscribing_label)
|
||||
}
|
||||
feedInFeedlist() -> {
|
||||
binding.subscribeButton.isEnabled = true
|
||||
binding.subscribeButton.setText(R.string.open)
|
||||
if (didPressSubscribe) {
|
||||
didPressSubscribe = false
|
||||
|
||||
val feed1 = DBReader.getFeed(feedId)?: return
|
||||
val feedPreferences = feed1.preferences
|
||||
if (feedPreferences != null) {
|
||||
if (isEnableAutodownload) {
|
||||
val autoDownload = binding.autoDownloadCheckBox.isChecked
|
||||
feedPreferences.autoDownload = autoDownload
|
||||
|
||||
val preferences = requireContext().getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
val editor = preferences.edit()
|
||||
editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload)
|
||||
editor.apply()
|
||||
}
|
||||
if (username != null) {
|
||||
feedPreferences.username = username
|
||||
feedPreferences.password = password
|
||||
}
|
||||
DBWriter.setFeedPreferences(feedPreferences)
|
||||
}
|
||||
openFeed()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
binding.subscribeButton.isEnabled = true
|
||||
binding.subscribeButton.setText(R.string.subscribe_label)
|
||||
if (isEnableAutodownload) {
|
||||
binding.autoDownloadCheckBox.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun feedInFeedlist(): Boolean {
|
||||
return feedId != 0L
|
||||
}
|
||||
|
||||
private val feedId: Long
|
||||
get() {
|
||||
if (feeds == null) return 0
|
||||
|
||||
for (f in feeds!!) {
|
||||
if (f.download_url == selectedDownloadUrl) {
|
||||
return f.id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun showErrorDialog(errorMsg: String?, details: String) {
|
||||
if (!isRemoving && !isPaused) {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.error_label)
|
||||
if (errorMsg != null) {
|
||||
val total = """
|
||||
$errorMsg
|
||||
|
||||
$details
|
||||
""".trimIndent()
|
||||
val errorMessage = SpannableString(total)
|
||||
errorMessage.setSpan(ForegroundColorSpan(-0x77777778),
|
||||
errorMsg.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
builder.setMessage(errorMessage)
|
||||
} else {
|
||||
builder.setMessage(R.string.download_error_error_unknown)
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.cancel() }
|
||||
// if (intent.getBooleanExtra(ARG_WAS_MANUAL_URL, false)) {
|
||||
// builder.setNeutralButton(R.string.edit_url_menu) { _: DialogInterface?, _: Int -> editUrl() }
|
||||
// }
|
||||
builder.setOnCancelListener {
|
||||
// setResult(RESULT_ERROR)
|
||||
// finish()
|
||||
}
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
dialog = builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun editUrl() {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.edit_url_menu)
|
||||
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
|
||||
if (downloader != null) {
|
||||
dialogBinding.urlEditText.setText(downloader!!.downloadRequest.source)
|
||||
}
|
||||
builder.setView(dialogBinding.root)
|
||||
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||
setLoadingLayout()
|
||||
lookupUrlAndDownload(dialogBinding.urlEditText.text.toString())
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, _: Int -> dialog1.cancel() }
|
||||
builder.setOnCancelListener {}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found).
|
||||
*/
|
||||
private fun showFeedDiscoveryDialog(feedFile: File, baseUrl: String): Boolean {
|
||||
val fd = FeedDiscoverer()
|
||||
val urlsMap: Map<String, String>
|
||||
try {
|
||||
urlsMap = fd.findLinks(feedFile, baseUrl)
|
||||
if (urlsMap.isEmpty()) return false
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
if (isRemoving || isPaused) return false
|
||||
|
||||
val titles: MutableList<String?> = ArrayList()
|
||||
|
||||
val urls: List<String> = ArrayList(urlsMap.keys)
|
||||
for (url in urls) {
|
||||
titles.add(urlsMap[url])
|
||||
}
|
||||
|
||||
if (urls.size == 1) {
|
||||
// Skip dialog and display the item directly
|
||||
startFeedDownload(urls[0])
|
||||
return true
|
||||
}
|
||||
|
||||
val adapter = ArrayAdapter(requireContext(), R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles)
|
||||
val onClickListener = DialogInterface.OnClickListener { dialog: DialogInterface, which: Int ->
|
||||
val selectedUrl = urls[which]
|
||||
dialog.dismiss()
|
||||
startFeedDownload(selectedUrl)
|
||||
}
|
||||
|
||||
val ab = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.feeds_label)
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener { _: DialogInterface? ->
|
||||
// finish()
|
||||
}
|
||||
.setAdapter(adapter, onClickListener)
|
||||
|
||||
requireActivity().runOnUiThread {
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
dialog = ab.show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class FeedViewAuthenticationDialog(context: Context, titleRes: Int, private val feedUrl: String) :
|
||||
AuthenticationDialog(context, titleRes, true, username, password) {
|
||||
override fun onConfirmed(username: String, password: String) {
|
||||
this@OnlineFeedViewFragment.username = username
|
||||
this@OnlineFeedViewFragment.password = password
|
||||
startFeedDownload(feedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||
const val ARG_WAS_MANUAL_URL: String = "manual_url"
|
||||
private const val RESULT_ERROR = 2
|
||||
private const val TAG = "OnlineFeedViewFragment"
|
||||
private const val PREFS = "OnlineFeedViewFragmentPreferences"
|
||||
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
|
||||
private const val KEY_UP_ARROW = "up_arrow"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(feedUrl: String): OnlineFeedViewFragment {
|
||||
val fragment = OnlineFeedViewFragment()
|
||||
val b = Bundle()
|
||||
b.putString(ARG_FEEDURL, feedUrl)
|
||||
fragment.arguments = b
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FragmentItunesSearchBinding
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearcher
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
|
@ -15,13 +20,6 @@ import androidx.appcompat.widget.SearchView
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FragmentItunesSearchBinding
|
||||
import ac.mdiq.podcini.ui.activity.OnlineFeedViewActivity
|
||||
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearcher
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearcherRegistry
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
class OnlineSearchFragment : Fragment() {
|
||||
|
@ -70,11 +68,9 @@ class OnlineSearchFragment : Fragment() {
|
|||
//Show information about the podcast when the list item is clicked
|
||||
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
val podcast = searchResults!![position]
|
||||
if (podcast != null) {
|
||||
val intent = Intent(activity, OnlineFeedViewActivity::class.java)
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl)
|
||||
intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, true)
|
||||
startActivity(intent)
|
||||
if (podcast?.feedUrl != null) {
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
progressBar = binding.progressBar
|
||||
|
|
|
@ -3,15 +3,13 @@ package ac.mdiq.podcini.ui.fragment
|
|||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryBinding
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.OnlineFeedViewActivity
|
||||
import ac.mdiq.podcini.ui.adapter.FeedDiscoverAdapter
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.util.event.DiscoveryDefaultUpdateEvent
|
||||
import ac.mdiq.podcini.net.discovery.ItunesTopListLoader
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.FeedDiscoverAdapter
|
||||
import ac.mdiq.podcini.util.event.DiscoveryDefaultUpdateEvent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
|
@ -158,14 +156,12 @@ class QuickFeedDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
})
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
@OptIn(UnstableApi::class) override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
val podcast: PodcastSearchResult? = adapter.getItem(position)
|
||||
if (podcast?.feedUrl.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val intent = Intent(activity, OnlineFeedViewActivity::class.java)
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast!!.feedUrl)
|
||||
startActivity(intent)
|
||||
if (podcast?.feedUrl.isNullOrEmpty()) return
|
||||
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast!!.feedUrl!!)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,9 +1,29 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||
import ac.mdiq.podcini.databinding.SearchFragmentBinding
|
||||
import ac.mdiq.podcini.net.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
|
||||
import ac.mdiq.podcini.storage.FeedSearcher
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodeItemListAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.HorizontalFeedListAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.fragment.actions.EpisodeMultiSelectActionHandler
|
||||
import ac.mdiq.podcini.ui.menuhandler.FeedItemMenuHandler
|
||||
import ac.mdiq.podcini.ui.menuhandler.FeedMenuHandler
|
||||
import ac.mdiq.podcini.ui.menuhandler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.view.EmptyViewHandler
|
||||
import ac.mdiq.podcini.ui.view.EpisodeItemListRecyclerView
|
||||
import ac.mdiq.podcini.ui.view.LiftOnScrollListener
|
||||
import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder
|
||||
import ac.mdiq.podcini.util.FeedItemUtil
|
||||
import ac.mdiq.podcini.util.event.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
@ -22,28 +42,6 @@ import com.google.android.material.chip.Chip
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.activity.OnlineFeedViewActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodeItemListAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.HorizontalFeedListAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.menuhandler.MenuItemUtils
|
||||
import ac.mdiq.podcini.storage.FeedSearcher
|
||||
import ac.mdiq.podcini.util.FeedItemUtil
|
||||
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||
import ac.mdiq.podcini.databinding.SearchFragmentBinding
|
||||
import ac.mdiq.podcini.util.event.*
|
||||
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
|
||||
import ac.mdiq.podcini.ui.fragment.actions.EpisodeMultiSelectActionHandler
|
||||
import ac.mdiq.podcini.ui.menuhandler.FeedItemMenuHandler
|
||||
import ac.mdiq.podcini.ui.menuhandler.FeedMenuHandler
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.net.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.ui.view.EmptyViewHandler
|
||||
import ac.mdiq.podcini.ui.view.EpisodeItemListRecyclerView
|
||||
import ac.mdiq.podcini.ui.view.LiftOnScrollListener
|
||||
import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
@ -140,7 +138,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
|||
requireArguments().putLong(ARG_FEED, 0)
|
||||
searchWithProgressBar()
|
||||
}
|
||||
chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE
|
||||
chip.visibility = if (requireArguments().getLong(ARG_FEED, 0) == 0L) View.GONE else View.VISIBLE
|
||||
chip.text = requireArguments().getString(ARG_FEED_NAME, "")
|
||||
if (requireArguments().getString(ARG_QUERY, null) != null) {
|
||||
search()
|
||||
|
@ -295,7 +293,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
|||
if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem)
|
||||
currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||
else {
|
||||
Log.d(FeedItemlistFragment.TAG, "onEventMainThread() search list")
|
||||
Log.d(TAG, "onEventMainThread() search list")
|
||||
for (i in 0 until adapter.itemCount) {
|
||||
val holder: EpisodeItemViewHolder? =
|
||||
recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
|
||||
|
@ -368,9 +366,8 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
|||
inVal.hideSoftInputFromWindow(searchView.windowToken, 0)
|
||||
val query = searchView.query.toString()
|
||||
if (query.matches("http[s]?://.*".toRegex())) {
|
||||
val intent = Intent(activity, OnlineFeedViewActivity::class.java)
|
||||
intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, query)
|
||||
startActivity(intent)
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(query)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
return
|
||||
}
|
||||
(activity as MainActivity).loadChildFragment(
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:elevation="0dp">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="?attr/actionBarTheme"
|
||||
app:title="@string/online_feed"
|
||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/transparentBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardCornerRadius="8dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
style="?android:attr/progressBarStyle" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/feed_display_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/feeditemlist_header_height"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@color/feed_image_bg">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backgroundImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/coverImage"
|
||||
android:layout_width="@dimen/thumbnail_length_onlinefeedview"
|
||||
android:layout_height="@dimen/thumbnail_length_onlinefeedview"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:background="@drawable/bg_rounded_corners"
|
||||
android:clipToOutline="true"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleLabel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textFontWeight="800"
|
||||
tools:text="Podcast title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/author_label"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/titleLabel"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
tools:text="Podcast author" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/author_label"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<Button
|
||||
android:id="@+id/subscribeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/subscribe_label" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/episodeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/episodes_label"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_close_white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/alternate_urls_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:dropDownWidth="match_parent"
|
||||
android:padding="8dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="@dimen/text_size_micro" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/autoDownloadCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left"
|
||||
android:checked="true"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/auto_download_label"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/online_feed_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/description_label"
|
||||
style="@style/TextAppearance.Material3.TitleMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:ellipsize="end"
|
||||
android:lineHeight="20dp"
|
||||
style="@style/Podcini.TextView.ListItemBody"
|
||||
tools:text="@string/design_time_lorem_ipsum" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -1,230 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/transparentBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardCornerRadius="8dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
style="?android:attr/progressBarStyle" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/feed_display_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/feeditemlist_header_height"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@color/feed_image_bg">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backgroundImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/coverImage"
|
||||
android:layout_width="@dimen/thumbnail_length_onlinefeedview"
|
||||
android:layout_height="@dimen/thumbnail_length_onlinefeedview"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:background="@drawable/bg_rounded_corners"
|
||||
android:clipToOutline="true"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleLabel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textFontWeight="800"
|
||||
tools:text="Podcast title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/author_label"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/titleLabel"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
tools:text="Podcast author" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/author_label"
|
||||
android:layout_toRightOf="@id/coverImage"
|
||||
android:layout_toEndOf="@id/coverImage"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<Button
|
||||
android:id="@+id/subscribeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/subscribe_label" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/episodeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/episodes_label"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_close_white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/alternate_urls_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:dropDownWidth="match_parent"
|
||||
android:padding="8dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="@dimen/text_size_micro" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/autoDownloadCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="left"
|
||||
android:checked="true"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/auto_download_label"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- <Button-->
|
||||
<!-- android:id="@+id/stopPreviewButton"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginTop="4dp"-->
|
||||
<!-- android:text="@string/stop_preview"-->
|
||||
<!-- android:visibility="gone"-->
|
||||
<!-- tools:visibility="visible" />-->
|
||||
</LinearLayout>
|
||||
|
||||
<!-- <ListView-->
|
||||
<!-- android:id="@+id/listView"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- android:paddingTop="16dp"-->
|
||||
<!-- android:clipToPadding="false" />-->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/online_feed_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/description_label"
|
||||
style="@style/TextAppearance.Material3.TitleMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvDescription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:ellipsize="end"
|
||||
android:lineHeight="20dp"
|
||||
style="@style/Podcini.TextView.ListItemBody"
|
||||
tools:text="@string/design_time_lorem_ipsum" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
|
@ -670,6 +670,7 @@
|
|||
<string name="pref_pausePlaybackForFocusLoss_title">Pause for interruptions</string>
|
||||
|
||||
<!-- Online feed view -->
|
||||
<string name="online_feed">Online feed</string>
|
||||
<string name="subscribe_label">Subscribe</string>
|
||||
<string name="subscribing_label">Subscribing…</string>
|
||||
<string name="preview_episode">Preview</string>
|
||||
|
|
11
changelog.md
11
changelog.md
|
@ -171,4 +171,13 @@
|
|||
* converted ksp back to kapt
|
||||
* unrestricted the titles to 2 lines in player details view
|
||||
* fixed once again the bug of player disappear on first play
|
||||
* some code refactoring
|
||||
* some code refactoring
|
||||
|
||||
## 4.4.3
|
||||
|
||||
* created online feed view fragment
|
||||
* online episodes list view is no longer restricted to 50 episodes
|
||||
* online episodes list view now better handles icons
|
||||
* online episodes list view goes back to the online feed view
|
||||
* the original online feed view activity is only preserved for receiving shared feed
|
||||
* externally shared feed opens in the online feed view fragment
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
Version 4.4.3 brings several changes:
|
||||
|
||||
* created online feed view fragment
|
||||
* online episodes list view is no longer restricted to 50 episodes
|
||||
* online episodes list view now better handles icons
|
||||
* online episodes list view goes back to the online feed view
|
||||
* the original online feed view activity is only preserved for receiving shared feed
|
||||
* externally shared feed opens in the online feed view fragment
|
Loading…
Reference in New Issue