4.4.1 commit

This commit is contained in:
Xilin Jia 2024-03-27 13:34:42 +00:00
parent c3f70fd1d4
commit e08d508c41
35 changed files with 377 additions and 295 deletions

View File

@ -149,8 +149,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020116
versionName "4.4.0"
versionCode 3020117
versionName "4.4.1"
def commit = ""
try {

View File

@ -30,6 +30,9 @@
android:name="android.hardware.touchscreen"
android:required="false"/>
<!-- this is now taken out from application -->
<!-- android:usesCleartextTraffic="true"-->
<application
android:name="ac.mdiq.podcini.PodciniApp"
android:icon="@mipmap/ic_launcher"
@ -38,7 +41,6 @@
android:backupAgent=".storage.backup.OpmlBackupAgent"
android:restoreAnyVersion="true"
android:theme="@style/Theme.Podcini.Splash"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
android:logo="@mipmap/ic_launcher"
android:resizeableActivity="true"

View File

@ -3,17 +3,16 @@ package ac.mdiq.podcini.net.discovery
import io.reactivex.Single
object PodcastSearcherRegistry {
@Suppress("UNNECESSARY_SAFE_CALL")
@get:Synchronized
var searchProviders: MutableList<SearcherInfo> = mutableListOf()
get() {
if (field.isEmpty()) {
field = ArrayList()
field?.add(SearcherInfo(CombinedSearcher(), 1.0f))
field?.add(SearcherInfo(GpodnetPodcastSearcher(), 0.0f))
field?.add(SearcherInfo(FyydPodcastSearcher(), 1.0f))
field?.add(SearcherInfo(ItunesPodcastSearcher(), 1.0f))
field?.add(SearcherInfo(PodcastIndexPodcastSearcher(), 1.0f))
field.add(SearcherInfo(CombinedSearcher(), 1.0f))
field.add(SearcherInfo(GpodnetPodcastSearcher(), 0.0f))
field.add(SearcherInfo(FyydPodcastSearcher(), 1.0f))
field.add(SearcherInfo(ItunesPodcastSearcher(), 1.0f))
field.add(SearcherInfo(PodcastIndexPodcastSearcher(), 1.0f))
}
return field
}

View File

@ -67,9 +67,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
@Synchronized
private fun initServiceRunning() {
if (initialized) {
return
}
if (initialized) return
initialized = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -171,7 +169,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Log.d(TAG, "Received statusUpdate Intent.")
Log.d(TAG, "Received statusUpdate Intent.")
if (playbackService != null) {
val info = playbackService!!.pSMPInfo
status = info.playerStatus
@ -271,7 +269,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
PlaybackServiceStarter(activity, media!!)
.callEvenIfRunning(true)
.start()
Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!")
Log.w(TAG, "playbackservice was null, restarted!")
// return
}
when (status) {

View File

@ -84,88 +84,101 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
isGzip = TextUtils.equals(contentEncodingHeader.lowercase(Locale.getDefault()), "gzip")
}
Log.d(TAG, "Response code is " + response.code)
if (!response.isSuccessful && response.code == HttpURLConnection.HTTP_NOT_MODIFIED) {
Log.d(TAG, "Feed '" + downloadRequest.source + "' not modified since last update, Download canceled")
onCancelled()
return
} else if (!response.isSuccessful || response.body == null) {
callOnFailByResponseCode(response)
return
} else if (downloadRequest.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA && isContentTypeTextAndSmallerThan100kb(response)) {
onFail(DownloadError.ERROR_FILE_TYPE, null)
return
}
checkIfRedirect(response)
connection = BufferedInputStream(responseBody!!.byteStream())
val contentRangeHeader = if ((fileExists)) response.header("Content-Range") else null
if (fileExists && response.code == HttpURLConnection.HTTP_PARTIAL && !contentRangeHeader.isNullOrEmpty()) {
val start = contentRangeHeader.substring("bytes ".length, contentRangeHeader.indexOf("-"))
downloadRequest.soFar = start.toLong()
Log.d(TAG, "Starting download at position " + downloadRequest.soFar)
out = RandomAccessFile(destination, "rw")
out.seek(downloadRequest.soFar)
} else {
var success = destination.delete()
success = success or destination.createNewFile()
if (!success) {
throw IOException("Unable to recreate partially downloaded file")
}
out = RandomAccessFile(destination, "rw")
}
val buffer = ByteArray(BUFFER_SIZE)
var count = 0
downloadRequest.setStatusMsg(R.string.download_running)
Log.d(TAG, "Getting size of download")
downloadRequest.size = responseBody.contentLength() + downloadRequest.soFar
Log.d(TAG, "Size is " + downloadRequest.size)
if (downloadRequest.size < 0) {
downloadRequest.size = DownloadResult.SIZE_UNKNOWN.toLong()
}
val freeSpace = freeSpaceAvailable
Log.d(TAG, "Free space is $freeSpace")
if (downloadRequest.size != DownloadResult.SIZE_UNKNOWN.toLong() && downloadRequest.size > freeSpace) {
onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null)
return
}
Log.d(TAG, "Starting download")
try {
while (!cancelled && (connection.read(buffer).also { count = it }) != -1) {
// Log.d(TAG,"buffer: $buffer")
out.write(buffer, 0, count)
downloadRequest.soFar += count
val progressPercent = (100.0 * downloadRequest.soFar / downloadRequest.size).toInt()
downloadRequest.progressPercent = progressPercent
}
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
}
if (cancelled) {
onCancelled()
} else {
// check if size specified in the response header is the same as the size of the
// written file. This check cannot be made if compression was used
if (!isGzip && downloadRequest.size != DownloadResult.SIZE_UNKNOWN.toLong() && downloadRequest.soFar != downloadRequest.size) {
onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: "
+ downloadRequest.soFar + " does not equal expected size " + downloadRequest.size)
return
} else if (downloadRequest.size > 0 && downloadRequest.soFar == 0L) {
onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read")
Log.d(TAG, "Response code is " + response.code)// check if size specified in the response header is the same as the size of the
// written file. This check cannot be made if compression was used
// Log.d(TAG,"buffer: $buffer")
when {
!response.isSuccessful && response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> {
Log.d(TAG, "Feed '" + downloadRequest.source + "' not modified since last update, Download canceled")
onCancelled()
return
}
val lastModified = response.header("Last-Modified")
if (lastModified != null) {
downloadRequest.setLastModified(lastModified)
} else {
downloadRequest.setLastModified(response.header("ETag"))
!response.isSuccessful || response.body == null -> {
callOnFailByResponseCode(response)
return
}
downloadRequest.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA && isContentTypeTextAndSmallerThan100kb(response) -> {
onFail(DownloadError.ERROR_FILE_TYPE, null)
return
}
else -> {
checkIfRedirect(response)
connection = BufferedInputStream(responseBody!!.byteStream())
val contentRangeHeader = if ((fileExists)) response.header("Content-Range") else null
if (fileExists && response.code == HttpURLConnection.HTTP_PARTIAL && !contentRangeHeader.isNullOrEmpty()) {
val start = contentRangeHeader.substring("bytes ".length, contentRangeHeader.indexOf("-"))
downloadRequest.soFar = start.toLong()
Log.d(TAG, "Starting download at position " + downloadRequest.soFar)
out = RandomAccessFile(destination, "rw")
out.seek(downloadRequest.soFar)
} else {
var success = destination.delete()
success = success or destination.createNewFile()
if (!success) {
throw IOException("Unable to recreate partially downloaded file")
}
out = RandomAccessFile(destination, "rw")
}
val buffer = ByteArray(BUFFER_SIZE)
var count = 0
downloadRequest.setStatusMsg(R.string.download_running)
Log.d(TAG, "Getting size of download")
downloadRequest.size = responseBody.contentLength() + downloadRequest.soFar
Log.d(TAG, "Size is " + downloadRequest.size)
if (downloadRequest.size < 0) {
downloadRequest.size = DownloadResult.SIZE_UNKNOWN.toLong()
}
val freeSpace = freeSpaceAvailable
Log.d(TAG, "Free space is $freeSpace")
if (downloadRequest.size != DownloadResult.SIZE_UNKNOWN.toLong() && downloadRequest.size > freeSpace) {
onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null)
return
}
Log.d(TAG, "Starting download")
try {
while (!cancelled && (connection.read(buffer).also { count = it }) != -1) {
// Log.d(TAG,"buffer: $buffer")
out.write(buffer, 0, count)
downloadRequest.soFar += count
val progressPercent = (100.0 * downloadRequest.soFar / downloadRequest.size).toInt()
downloadRequest.progressPercent = progressPercent
}
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
}
if (cancelled) {
onCancelled()
} else {
// check if size specified in the response header is the same as the size of the
// written file. This check cannot be made if compression was used
when {
!isGzip && downloadRequest.size != DownloadResult.SIZE_UNKNOWN.toLong() && downloadRequest.soFar != downloadRequest.size -> {
onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: "
+ downloadRequest.soFar + " does not equal expected size " + downloadRequest.size)
return
}
downloadRequest.size > 0 && downloadRequest.soFar == 0L -> {
onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read")
return
}
else -> {
val lastModified = response.header("Last-Modified")
if (lastModified != null) {
downloadRequest.setLastModified(lastModified)
} else {
downloadRequest.setLastModified(response.header("ETag"))
}
onSuccess()
}
}
}
}
onSuccess()
}
} catch (e: IllegalArgumentException) {
e.printStackTrace()

View File

@ -139,12 +139,13 @@ class NewEpisodesNotification {
private fun loadIcon(context: Context, feed: Feed): Bitmap? {
val iconSize = (128 * context.resources.displayMetrics.density).toInt()
return try {
Glide.with(context)
if (!feed.imageUrl.isNullOrBlank()) Glide.with(context)
.asBitmap()
.load(feed.imageUrl)
.apply(RequestOptions().centerCrop())
.submit(iconSize, iconSize)
.get()
else null
} catch (tr: Throwable) {
null
}

View File

@ -64,7 +64,7 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
val options = RequestOptions().centerCrop()
try {
var imgLoc = playable?.getImageLocation()
if (imgLoc != null) {
if (!imgLoc.isNullOrBlank()) {
cachedIcon = Glide.with(context)
.asBitmap()
.load(imgLoc)
@ -73,7 +73,7 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
.get()
} else if (playable != null) {
imgLoc = ImageResourceUtils.getFallbackImageLocation(playable!!)
if (imgLoc != null) {
if (!imgLoc.isNullOrBlank()) {
cachedIcon = Glide.with(context)
.asBitmap()
.load(imgLoc)
@ -92,8 +92,7 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
private val defaultIcon: Bitmap?
get() {
if (Companion.defaultIcon == null) {
Companion.defaultIcon = getBitmap(
context, R.mipmap.ic_launcher)
Companion.defaultIcon = getBitmap(context, R.mipmap.ic_launcher)
}
return Companion.defaultIcon
}

View File

@ -20,7 +20,7 @@ import java.util.*
class RemoteMedia : Playable {
private var itemIdentifier: String? = null
private val downloadUrl: String? = null
private val imageUrl: String? = null
private val imageUrl: String?
private val notes: String? = null
private val streamUrl: String?
@ -52,6 +52,7 @@ class RemoteMedia : Playable {
this.episodeLink = episodeLink
this.feedAuthor = feedAuthor
this.imageLocation = imageUrl
this.imageUrl = imageUrl
this.feedLink = feedLink
this.mimeType = mimeType
this.pubDate = pubDate
@ -67,9 +68,11 @@ class RemoteMedia : Playable {
this.episodeLink = item.link
this.feedAuthor = item.feed?.author
if (!item.imageUrl.isNullOrEmpty()) {
this.imageLocation = item.imageUrl
this.imageLocation = item.imageLocation
this.imageUrl = item.imageUrl
} else {
this.imageLocation = item.feed?.imageUrl
this.imageUrl = item.feed?.imageUrl
}
this.feedLink = item.feed?.link
this.mimeType = item.media?.mime_type
@ -181,7 +184,7 @@ class RemoteMedia : Playable {
}
override fun getImageLocation(): String? {
return imageUrl
return imageLocation
}
override fun describeContents(): Int {

View File

@ -176,12 +176,9 @@ class OnlineFeedViewActivity : AppCompatActivity() {
super.onStop()
isPaused = true
EventBus.getDefault().unregister(this)
if (downloader != null && !downloader!!.isFinished) {
downloader!!.cancel()
}
if (dialog != null && dialog!!.isShowing) {
dialog!!.dismiss()
}
if (downloader != null && !downloader!!.isFinished) downloader!!.cancel()
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
}
public override fun onDestroy() {
@ -329,7 +326,6 @@ class OnlineFeedViewActivity : AppCompatActivity() {
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableMaybeObserver<FeedHandlerResult?>() {
@UnstableApi override fun onSuccess(result: FeedHandlerResult) {
showFeedInformation(result.feed, result.alternateFeedUrls)
}
@ -398,7 +394,7 @@ class OnlineFeedViewActivity : AppCompatActivity() {
binding.episodeLabel.setOnClickListener { showEpisodes(feed.items)}
if (StringUtils.isNotBlank(feed.imageUrl)) {
if (!feed.imageUrl.isNullOrBlank()) {
Glide.with(this)
.load(feed.imageUrl)
.apply(RequestOptions()
@ -499,40 +495,44 @@ class OnlineFeedViewActivity : AppCompatActivity() {
val dli = DownloadServiceInterface.get()
if (dli == null || selectedDownloadUrl == null) return
if (dli.isDownloadingEpisode(selectedDownloadUrl!!)) {
binding.subscribeButton.isEnabled = false
binding.subscribeButton.setText(R.string.subscribing_label)
} else if (feedInFeedlist()) {
binding.subscribeButton.isEnabled = true
binding.subscribeButton.setText(R.string.open_podcast)
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()
when {
dli.isDownloadingEpisode(selectedDownloadUrl!!) -> {
binding.subscribeButton.isEnabled = false
binding.subscribeButton.setText(R.string.subscribing_label)
}
} else {
binding.subscribeButton.isEnabled = true
binding.subscribeButton.setText(R.string.subscribe_label)
if (isEnableAutodownload) {
binding.autoDownloadCheckBox.visibility = View.VISIBLE
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
}
}
}
}
@ -579,9 +579,7 @@ class OnlineFeedViewActivity : AppCompatActivity() {
setResult(RESULT_ERROR)
finish()
}
if (dialog != null && dialog!!.isShowing) {
dialog!!.dismiss()
}
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
dialog = builder.show()
}
}
@ -615,17 +613,13 @@ class OnlineFeedViewActivity : AppCompatActivity() {
val urlsMap: Map<String, String>
try {
urlsMap = fd.findLinks(feedFile, baseUrl)
if (urlsMap.isEmpty()) {
return false
}
if (urlsMap.isEmpty()) return false
} catch (e: IOException) {
e.printStackTrace()
return false
}
if (isPaused || isFinishing) {
return false
}
if (isPaused || isFinishing) return false
val titles: MutableList<String?> = ArrayList()
@ -657,9 +651,7 @@ class OnlineFeedViewActivity : AppCompatActivity() {
.setAdapter(adapter, onClickListener)
runOnUiThread {
if (dialog != null && dialog!!.isShowing) {
dialog!!.dismiss()
}
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
dialog = ab.show()
}
return true

View File

@ -106,7 +106,7 @@ class SelectSubscriptionActivity : AppCompatActivity() {
private fun getBitmapFromUrl(feed: Feed) {
val iconSize = (128 * resources.displayMetrics.density).toInt()
Glide.with(this)
if (!feed.imageUrl.isNullOrBlank()) Glide.with(this)
.asBitmap()
.load(feed.imageUrl)
.apply(RequestOptions.overrideOf(iconSize, iconSize))

View File

@ -89,12 +89,15 @@ class ChaptersListAdapter(private val context: Context, private val callback: Ca
if (sc.imageUrl.isNullOrEmpty()) {
Glide.with(context).clear(holder.image)
} else {
if (media != null) Glide.with(context)
.load(EmbeddedChapterImage.getModelFor(media!!, position))
.apply(RequestOptions()
.dontAnimate()
.transform(FitCenter(), RoundedCorners((4 * context.resources.displayMetrics.density).toInt())))
.into(holder.image)
if (media != null) {
val imgUrl = EmbeddedChapterImage.getModelFor(media!!,position)
if (imgUrl != null) Glide.with(context)
.load(imgUrl)
.apply(RequestOptions()
.dontAnimate()
.transform(FitCenter(), RoundedCorners((4 * context.resources.displayMetrics.density).toInt())))
.into(holder.image)
}
}
} else {
holder.image.visibility = View.GONE

View File

@ -77,7 +77,7 @@ class CoverLoader(activity: MainActivity) {
.load(uri)
.apply(options)
if (fallbackUri != null) {
if (!fallbackUri.isNullOrBlank()) {
builder = builder.error(Glide.with(imgvCover!!)
.`as`(Drawable::class.java)
.load(fallbackUri)

View File

@ -53,7 +53,7 @@ class FeedDiscoverAdapter(mainActivity: MainActivity) : BaseAdapter() {
val podcast: PodcastSearchResult? = getItem(position)
holder.imageView!!.contentDescription = podcast?.title
Glide.with(mainActivityRef.get()!!)
if (!podcast?.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
.load(podcast?.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)

View File

@ -77,7 +77,7 @@ open class HorizontalFeedListAdapter(mainActivity: MainActivity) :
false
}
Glide.with(mainActivityRef.get()!!)
if (!podcast.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
.load(podcast.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)

View File

@ -287,7 +287,7 @@ class NavListAdapter(private val itemAccess: ItemAccess, context: Activity) :
val feed = drawerItem.feed
val context = activity.get() ?: return
Glide.with(context)
if (!feed.imageUrl.isNullOrBlank()) Glide.with(context)
.load(feed.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)

View File

@ -64,7 +64,7 @@ class OnlineFeedsAdapter(private val context: Context, objects: List<PodcastSear
} else viewHolder.updateView.visibility = View.INVISIBLE
//Update the empty imageView with the image from the feed
Glide.with(context)
if (!podcast.imageUrl.isNullOrBlank()) Glide.with(context)
.load(podcast.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)

View File

@ -27,7 +27,7 @@ class SimpleIconListAdapter<T : SimpleIconListAdapter.ListItem>(private val cont
val binding = SimpleIconListItemBinding.bind(view!!)
binding.title.text = item.title
binding.subtitle.text = item.subtitle
Glide.with(context)
if (item.imageUrl.isNotBlank()) Glide.with(context)
.load(item.imageUrl)
.apply(RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)

View File

@ -8,6 +8,7 @@ import ac.mdiq.podcini.storage.DBTasks
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.playback.MediaType
import android.util.Log
class PlayActionButton(item: FeedItem) : ItemActionButton(item) {
override fun getLabel(): Int {
@ -18,6 +19,7 @@ class PlayActionButton(item: FeedItem) : ItemActionButton(item) {
}
@UnstableApi override fun onClick(context: Context) {
val media = item.media ?: return
Log.d("PlayActionButton", "onClick called")
if (!media.fileExists()) {
DBTasks.notifyMissingFeedMediaFile(context, media)
return

View File

@ -121,8 +121,7 @@ class SwipeActionsDialog(private val context: Context, private val tag: String)
for (i in keys.indices) {
val action = keys[i]
val item = SwipeactionsPickerItemBinding.inflate(LayoutInflater.from(
context))
val item = SwipeactionsPickerItemBinding.inflate(LayoutInflater.from(context))
item.swipeActionLabel.text = action.getTitle(context)
val icon = DrawableCompat.wrap(AppCompatResources.getDrawable(context, action.getActionIcon())!!)

View File

@ -71,6 +71,8 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect
lateinit var listAdapter: EpisodeItemListAdapter
protected lateinit var txtvInformation: TextView
private var currentPlaying: EpisodeItemViewHolder? = null
@JvmField
var episodes: MutableList<FeedItem> = ArrayList()
@ -327,7 +329,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: FeedItemEvent) {
Log.d(TAG, "onEventMainThread() called with: event = [$event]")
Log.d(TAG, "onEventMainThread() called with FeedItemEvent event = [$event]")
for (item in event.items) {
val pos: Int = FeedItemUtil.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
@ -344,11 +346,19 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: PlaybackPositionEvent) {
for (i in 0 until listAdapter.itemCount) {
val holder: EpisodeItemViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
holder.notifyPlaybackPositionUpdated(event)
break
// Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]")
if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem)
currentPlaying!!.notifyPlaybackPositionUpdated(event)
else {
Log.d(TAG, "onEventMainThread() search list")
for (i in 0 until listAdapter.itemCount) {
val holder: EpisodeItemViewHolder? =
recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
currentPlaying = holder
holder.notifyPlaybackPositionUpdated(event)
break
}
}
}
}

View File

@ -70,7 +70,7 @@ class CompletedDownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeLis
private var disposable: Disposable? = null
private var displayUpArrow = false
private var currentPlaying: EpisodeItemViewHolder? = null
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -272,12 +272,19 @@ class CompletedDownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeLis
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: PlaybackPositionEvent) {
// if (event == null) return
for (i in 0 until adapter.itemCount) {
val holder: EpisodeItemViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
holder.notifyPlaybackPositionUpdated(event)
break
// Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]")
if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem)
currentPlaying!!.notifyPlaybackPositionUpdated(event)
else {
Log.d(TAG, "onEventMainThread() search list")
for (i in 0 until adapter.itemCount) {
val holder: EpisodeItemViewHolder? =
recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
currentPlaying = holder
holder.notifyPlaybackPositionUpdated(event)
break
}
}
}
refreshInfoBar()

View File

@ -35,6 +35,7 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
@ -189,7 +190,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val balloon: Balloon = Balloon.Builder(requireContext())
.setArrowOrientation(ArrowOrientation.TOP)
.setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED)
.setArrowPosition(0.25f + (if ((isLocaleRtl xor offerStreaming)) 0f else 0.5f))
.setArrowPosition(0.25f + (if (isLocaleRtl xor offerStreaming) 0f else 0.5f))
.setWidthRatio(1.0f)
.setMarginLeft(8)
.setMarginRight(8)
@ -199,10 +200,10 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
.setDismissWhenTouchOutside(true)
.setLifecycleOwner(this)
.build()
val binding_ = PopupBubbleViewBinding.bind(balloon.getContentView())
val positiveButton = binding_.balloonButtonPositive
val negativeButton = binding_.balloonButtonNegative
val message: TextView = binding_.balloonMessage
val ballonView = balloon.getContentView()
val positiveButton = ballonView.findViewById(R.id.balloon_button_positive) as Button
val negativeButton = ballonView.findViewById(R.id.balloon_button_negative) as Button
val message: TextView = ballonView.findViewById(R.id.balloon_message) as TextView
message.setText(if (offerStreaming) R.string.on_demand_config_stream_text
else R.string.on_demand_config_download_text)
positiveButton.setOnClickListener {
@ -297,13 +298,15 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
RoundedCorners((8 * resources.displayMetrics.density).toInt()))
.dontAnimate()
Glide.with(this)
.load(item!!.imageLocation)
.error(Glide.with(this)
.load(ImageResourceUtils.getFallbackImageLocation(item!!))
.apply(options))
.apply(options)
.into(imgvCover)
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(item!!)
if (!item!!.imageLocation.isNullOrBlank() || !imgLocFB.isNullOrBlank())
Glide.with(this)
.load(item!!.imageLocation)
.error(Glide.with(this)
.load(imgLocFB)
.apply(options))
.apply(options)
.into(imgvCover)
updateButtons()
}
@ -398,15 +401,9 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@UnstableApi @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: EpisodeDownloadEvent) {
if (item == null || item!!.media == null) {
return
}
if (!event.urls.contains(item!!.media!!.download_url)) {
return
}
if (itemsLoaded && activity != null) {
updateButtons()
}
if (item == null || item!!.media == null) return
if (!event.urls.contains(item!!.media!!.download_url)) return
if (itemsLoaded && activity != null) updateButtons()
}
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)

View File

@ -1,15 +1,9 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.preferences.UserPreferences.allEpisodesSortOrder
import ac.mdiq.podcini.preferences.UserPreferences.prefFilterAllEpisodes
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.storage.model.feed.SortOrder
import ac.mdiq.podcini.ui.dialog.AllEpisodesFilterDialog
import ac.mdiq.podcini.ui.dialog.AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent
import ac.mdiq.podcini.ui.dialog.ItemSortDialog
import ac.mdiq.podcini.util.event.FeedListUpdateEvent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -18,8 +12,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import org.apache.commons.lang3.StringUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import kotlin.math.min
@ -49,6 +41,7 @@ class EpisodesListFragment : BaseEpisodesListFragment() {
}
override fun loadData(): List<FeedItem> {
if (episodeList.isEmpty()) return listOf()
return episodeList.subList(0, min(episodeList.size-1, page * EPISODES_PER_PAGE))
}

View File

@ -76,6 +76,8 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private var showTimeLeft = false
private var currentMedia: Playable? = null
private var controller: PlaybackController? = null
private var disposable: Disposable? = null
@ -192,6 +194,7 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
}
override fun loadMediaInfo() {
Log.d(TAG, "setupPlaybackController loadMediaInfo called")
this@ExternalPlayerFragment.loadMediaInfo()
}
@ -284,19 +287,24 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
@UnstableApi
private fun loadMediaInfo() {
Log.d(TAG, "Loading media info")
Log.d(TAG, "loadMediaInfo called")
if (controller == null) {
Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!")
return
}
disposable?.dispose()
disposable = Maybe.fromCallable<Playable?> { controller?.getMedia() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ media: Playable? -> this.updateUi(media) },
{ error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) },
{ (activity as MainActivity).setPlayerVisible(false) })
val theMedia = controller?.getMedia()
if (currentMedia == null || theMedia?.getIdentifier() != currentMedia?.getIdentifier()) {
Log.d(TAG, "reloading media info")
disposable?.dispose()
disposable = Maybe.fromCallable<Playable?> { theMedia }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ media: Playable? ->
currentMedia = media
this.updateUi(media) },
{ error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) },
{ (activity as MainActivity).setPlayerVisible(false) })
}
}
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
@ -354,9 +362,8 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
@UnstableApi
private fun updateUi(media: Playable?) {
if (media == null) {
return
}
if (media == null) return
episodeTitle.text = media.getEpisodeTitle()
(activity as MainActivity).setPlayerVisible(true)
onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration()))
@ -367,13 +374,19 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
.fitCenter()
.dontAnimate()
Glide.with(this)
.load(getEpisodeListImageLocation(media))
.error(Glide.with(this)
.load(getFallbackImageLocation(media))
.apply(options))
.apply(options)
.into(imgvCover)
val imgLoc = getEpisodeListImageLocation(media)
val imgLocFB = getFallbackImageLocation(media)
if (!imgLoc.isNullOrBlank())
Glide.with(this)
.load(imgLoc)
.apply(options)
.into(imgvCover)
else if (!imgLocFB.isNullOrBlank())
Glide.with(this)
.load(imgLocFB)
.apply(options)
.into(imgvCover)
else imgvCover.setImageResource(R.mipmap.ic_launcher)
if (controller?.isPlayingVideoLocally == true) {
(activity as MainActivity).bottomSheet.setLocked(true)

View File

@ -186,22 +186,24 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Log.d(TAG, "Language is " + feed!!.language)
Log.d(TAG, "Author is " + feed!!.author)
Log.d(TAG, "URL is " + feed!!.download_url)
Glide.with(this)
.load(feed!!.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(imgvCover)
Glide.with(this)
.load(feed!!.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
.transform(FastBlurTransformation())
.dontAnimate())
.into(imgvBackground)
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(imgvCover)
Glide.with(this)
.load(feed!!.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.image_readability_tint)
.error(R.color.image_readability_tint)
.transform(FastBlurTransformation())
.dontAnimate())
.into(imgvBackground)
}
txtvTitle.text = feed!!.title
txtvTitle.setMaxLines(6)

View File

@ -77,7 +77,9 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
private lateinit var adapter: FeedItemListAdapter
private lateinit var swipeActions: SwipeActions
private lateinit var nextPageLoader: MoreContentListFooterUtil
private var currentPlaying: EpisodeItemViewHolder? = null
private var displayUpArrow = false
private var headerCreated = false
private var feedID: Long = 0
@ -314,7 +316,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: FeedItemEvent) {
Log.d(TAG, "onEventMainThread() called with: event = [$event]")
Log.d(TAG, "onEventMainThread() called with FeedItemEvent event = [$event]")
if (feed == null || feed!!.items.isEmpty()) {
return
}
@ -334,7 +336,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: EpisodeDownloadEvent) {
Log.d(TAG, "onEventMainThread() called with: event = [$event]")
Log.d(TAG, "onEventMainThread() called with EpisodeDownloadEvent event = [$event]")
if (feed == null || feed!!.items.isEmpty()) {
return
}
@ -348,13 +350,19 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: PlaybackPositionEvent) {
Log.d(TAG, "onEventMainThread() called with: event = [$event]")
for (i in 0 until adapter.itemCount) {
val holder: EpisodeItemViewHolder? =
binding.recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
holder.notifyPlaybackPositionUpdated(event)
break
// Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]")
if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem)
currentPlaying!!.notifyPlaybackPositionUpdated(event)
else {
Log.d(TAG, "onEventMainThread() search list")
for (i in 0 until adapter.itemCount) {
val holder: EpisodeItemViewHolder? =
binding.recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
currentPlaying = holder
holder.notifyPlaybackPositionUpdated(event)
break
}
}
}
}
@ -521,24 +529,25 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
}
private fun loadFeedImage() {
if (feed == null) return
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.imgvBackground)
if (!feed?.imageUrl.isNullOrBlank()) {
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.imgvBackground)
Glide.with(this)
.load(feed!!.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(binding.header.imgvCover)
Glide.with(this)
.load(feed!!.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(binding.header.imgvCover)
}
}
@UnstableApi private fun loadItems() {
@ -657,7 +666,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
}
companion object {
const val TAG: String = "ItemlistFragment"
const val TAG: String = "FeedItemlistFragment"
private const val ARGUMENT_FEED_ID = "argument.ac.mdiq.podcini.feed_id"
private const val KEY_UP_ARROW = "up_arrow"

View File

@ -98,8 +98,7 @@ class OnlineSearchFragment : Fragment() {
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
}
) {}
})
return binding.root
}

View File

@ -279,8 +279,9 @@ class PlayerDetailsFragment : Fragment() {
if (displayedChapterIndex == -1 || media!!.getChapters().isEmpty() || media!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
cover.into(binding.imgvCover)
} else {
Glide.with(this)
.load(EmbeddedChapterImage.getModelFor(media!!, displayedChapterIndex))
val imgLoc = EmbeddedChapterImage.getModelFor(media!!, displayedChapterIndex)
if (imgLoc != null) Glide.with(this)
.load(imgLoc)
.apply(options)
.thumbnail(cover)
.error(cover)

View File

@ -81,6 +81,8 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
private var queue: MutableList<FeedItem> = mutableListOf()
private var recyclerAdapter: QueueRecyclerAdapter? = null
private var currentPlaying: EpisodeItemViewHolder? = null
private var disposable: Disposable? = null
override fun onCreate(savedInstanceState: Bundle?) {
@ -205,7 +207,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: QueueEvent) {
Log.d(TAG, "onEventMainThread() called with: event = [$event]")
Log.d(TAG, "onEventMainThread() called with QueueEvent event = [$event]")
if (recyclerAdapter == null) {
loadItems(true)
return
@ -242,7 +244,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: FeedItemEvent) {
Log.d(TAG, "onEventMainThread() called with: event = [$event]")
Log.d(TAG, "onEventMainThread() called with FeedItemEvent event = [$event]")
if (recyclerAdapter == null) {
loadItems(true)
return
@ -264,6 +266,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: EpisodeDownloadEvent) {
Log.d(TAG, "onEventMainThread() called with EpisodeDownloadEvent event = [$event]")
for (downloadUrl in event.urls) {
val pos: Int = FeedItemUtil.indexOfItemWithDownloadUrl(queue.toList(), downloadUrl)
if (pos >= 0) {
@ -274,12 +277,20 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: PlaybackPositionEvent) {
// Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]")
if (recyclerAdapter != null) {
for (i in 0 until recyclerAdapter!!.itemCount) {
val holder: EpisodeItemViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
holder.notifyPlaybackPositionUpdated(event)
break
if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem)
currentPlaying!!.notifyPlaybackPositionUpdated(event)
else {
Log.d(TAG, "onEventMainThread() search list")
for (i in 0 until recyclerAdapter!!.itemCount) {
val holder: EpisodeItemViewHolder? =
recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
currentPlaying = holder
holder.notifyPlaybackPositionUpdated(event)
break
}
}
}
}
@ -287,6 +298,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@Subscribe(threadMode = ThreadMode.MAIN)
fun onPlayerStatusChanged(event: PlayerStatusEvent?) {
Log.d(TAG, "onPlayerStatusChanged() called with event = [$event]")
loadItems(false)
refreshToolbarState()
}

View File

@ -70,6 +70,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
private lateinit var automaticSearchDebouncer: Handler
private var results: MutableList<FeedItem> = mutableListOf()
private var currentPlaying: EpisodeItemViewHolder? = null
private var disposable: Disposable? = null
private var lastQueryChange: Long = 0
@ -291,12 +292,18 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
@UnstableApi @Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: PlaybackPositionEvent) {
for (i in 0 until adapter.itemCount) {
val holder: EpisodeItemViewHolder? =
recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
holder.notifyPlaybackPositionUpdated(event)
break
if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem)
currentPlaying!!.notifyPlaybackPositionUpdated(event)
else {
Log.d(FeedItemlistFragment.TAG, "onEventMainThread() search list")
for (i in 0 until adapter.itemCount) {
val holder: EpisodeItemViewHolder? =
recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeItemViewHolder
if (holder != null && holder.isCurrentlyPlayingItem) {
currentPlaying = holder
holder.notifyPlaybackPositionUpdated(event)
break
}
}
}
}

View File

@ -51,7 +51,7 @@ abstract class StatisticsListAdapter protected constructor(@JvmField protected v
} else {
val holder = h as StatisticsHolder
val statsItem = statisticsData!![position - 1]
Glide.with(context)
if (!statsItem.feed.imageUrl.isNullOrBlank()) Glide.with(context)
.load(statsItem.feed.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)

View File

@ -120,12 +120,14 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou
}
if (coverHolder.visibility == View.VISIBLE) {
CoverLoader(activity)
.withUri(ImageResourceUtils.getEpisodeListImageLocation(item))
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(item)
if (!imgLoc.isNullOrBlank()) CoverLoader(activity)
.withUri(imgLoc)
.withFallbackUri(item.feed?.imageUrl)
.withPlaceholderView(placeholder)
.withCoverView(cover)
.load()
else cover.setImageResource(R.mipmap.ic_launcher)
}
}

View File

@ -187,6 +187,7 @@
<string name="filtered_label">Filtered</string>
<string name="refresh_failed_msg">{fa-exclamation-circle} Last refresh failed. Tap to view details.</string>
<string name="open_podcast">Open podcast</string>
<string name="open">Open</string>
<string name="please_wait_for_data">Please wait until the data is loaded</string>
<string name="updates_disabled_label">Updates disabled</string>
<plurals name="updated_feeds_batch_label">

View File

@ -155,4 +155,13 @@
* revamped online feed view activity
* episodes (50 most recent) of any online feed can be viewed and played (streamed) directly without subscribing to the feed
* bug fixes on passing Glide with null addresses
* null safety enhancements in code
* null safety enhancements in code
## 4.4.1
* fixed bug of app crash on stream episode customization
* disabled usesCleartextTraffic, connection to http sites appear OK, report if you find an issue
* enforced non-null load location for most Glide calls
* avoided redundant media loadings and ui updates when a new audio starts
* eliminated frequent list search during audio play, a serious energy waste
* icons in online episode list, when unavailable, are set to app logo

View File

@ -0,0 +1,9 @@
Version 4.4.1 brings several changes:
* fixed bug of app crash on stream episode customization
* disabled usesCleartextTraffic, connection to http sites appear OK, report if you find an issue
* enforced non-null load location for most Glide calls
* avoided redundant media loadings and ui updates when a new audio starts
* eliminated frequent list search during audio play, a serious energy waste
* icons in online episode list, when unavailable, are set to app logo