feed search and swipe telltales etc

This commit is contained in:
Xilin Jia 2024-03-21 21:00:47 +00:00
parent d5b5734712
commit e9c0e248e9
95 changed files with 1109 additions and 748 deletions

View File

@ -14,13 +14,15 @@ Version 4.1 brings a more convenient player control and tags enhancements, while
## Screenshots
<img src="./images/1_drawer1.jpg" width="238" /> <img src="./images/2_setting.jpg" width="238" /> <img src="./images/3_setting.jpg" width="238" />
<img src="./images/1_drawer.jpg" width="238" /> <img src="./images/2_setting.jpg" width="238" /> <img src="./images/2_setting1.jpg" width="238" />
<img src="./images/4_subscriptions.jpg" width="238" /> <img src="./images/5_queue.jpg" width="238" />
<img src="./images/3_subscriptions.jpg" width="238" /> <img src="./images/4_queue.jpg" width="238" />
<img src="./images/6_podcast.jpg" width="238" /> <img src="./images/7_podcast.jpg" width="238" /> <img src="./images/8_episode.jpg" width="238" />
<img src="./images/5_podcast.jpg" width="238" /> <img src="./images/5_podcast1.jpg" width="238" /> <img src="./images/6_episode.jpg" width="238" />
<img src="./images/9_speed.jpg" width="238" /> <img src="./images/10_player.jpg" width="238" />
<img src="./images/7_speed.jpg" width="238" /> <img src="./images/8_player.jpg" width="238" />
<img src="./images/9_swipe_setting.jpg" width="238" /> <img src="./images/9_swipe_setting1.jpg" width="238" /> <img src="./images/91_feed_search.jpg" width="238" />
## Changelogs

View File

@ -149,8 +149,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020110
versionName "4.2.7"
versionCode 3020111
versionName "4.3.0"
def commit = ""
try {

View File

@ -4,28 +4,21 @@ import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetPodcast
import de.mfietz.fyydlin.SearchHit
import org.json.JSONException
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
class PodcastSearchResult private constructor(
/**
* The name of the podcast
*/
val title: String,
/**
* URL of the podcast image
*/
val imageUrl: String?,
/**
* URL of the podcast feed
*/
val feedUrl: String?,
/**
* artistName of the podcast feed
*/
val author: String?
val author: String?,
val count: Int?,
val update: String?,
val source: String
) {
companion object {
fun dummy(): PodcastSearchResult {
return PodcastSearchResult("", "", "", "")
return PodcastSearchResult("", "", "", "", 0, "", "dummy")
}
/**
@ -39,7 +32,7 @@ class PodcastSearchResult private constructor(
val imageUrl: String? = json.optString("artworkUrl100").takeIf { it.isNotEmpty() }
val feedUrl: String? = json.optString("feedUrl").takeIf { it.isNotEmpty() }
val author: String? = json.optString("artistName").takeIf { it.isNotEmpty() }
return PodcastSearchResult(title, imageUrl, feedUrl, author)
return PodcastSearchResult(title, imageUrl, feedUrl, author, null, null, "Itunes")
}
/**
@ -71,21 +64,15 @@ class PodcastSearchResult private constructor(
} catch (e: Exception) {
// Some feeds have empty artist
}
return PodcastSearchResult(title, imageUrl, feedUrl, author)
return PodcastSearchResult(title, imageUrl, feedUrl, author, null, null, "Toplist")
}
fun fromFyyd(searchHit: SearchHit): PodcastSearchResult {
return PodcastSearchResult(searchHit.title,
searchHit.thumbImageURL,
searchHit.xmlUrl,
searchHit.author)
return PodcastSearchResult(searchHit.title, searchHit.thumbImageURL, searchHit.xmlUrl, searchHit.author, null, null, "Fyyd")
}
fun fromGpodder(searchHit: GpodnetPodcast): PodcastSearchResult {
return PodcastSearchResult(searchHit.title,
searchHit.logoUrl,
searchHit.url,
searchHit.author)
return PodcastSearchResult(searchHit.title, searchHit.logoUrl, searchHit.url, searchHit.author, null, null, "GPodder")
}
fun fromPodcastIndex(json: JSONObject): PodcastSearchResult {
@ -93,7 +80,15 @@ class PodcastSearchResult private constructor(
val imageUrl: String? = json.optString("image").takeIf { it.isNotEmpty() }
val feedUrl: String? = json.optString("url").takeIf { it.isNotEmpty() }
val author: String? = json.optString("author").takeIf { it.isNotEmpty() }
return PodcastSearchResult(title, imageUrl, feedUrl, author)
var count: Int? = json.optInt("episodeCount", -1)
if (count != null && count < 0) count = null
val updateInt: Int = json.optInt("lastUpdateTime", -1)
var update: String? = null
if (updateInt > 0) {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.US)
update = format.format(updateInt.toLong() * 1000)
}
return PodcastSearchResult(title, imageUrl, feedUrl, author, count, update, "PodcastIndex")
}
}
}

View File

@ -92,8 +92,7 @@ class PlaybackPreferences private constructor() : OnSharedPreferenceChangeListen
@JvmStatic
fun init(context: Context) {
instance = PlaybackPreferences()
prefs = PreferenceManager.getDefaultSharedPreferences(
context!!)
prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs?.registerOnSharedPreferenceChangeListener(instance)
}
@ -143,10 +142,9 @@ class PlaybackPreferences private constructor() : OnSharedPreferenceChangeListen
editor.putLong(PREF_CURRENTLY_PLAYING_MEDIA_TYPE, playable.getPlayableType().toLong())
editor.putBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, playable.getMediaType() == MediaType.VIDEO)
if (playable is FeedMedia) {
val feedMedia = playable
val itemId = feedMedia.getItem()?.feed?.id
val itemId = playable.getItem()?.feed?.id
if (itemId != null) editor.putLong(PREF_CURRENTLY_PLAYING_FEED_ID, itemId)
editor.putLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, feedMedia.id)
editor.putLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, playable.id)
} else {
editor.putLong(PREF_CURRENTLY_PLAYING_FEED_ID, NO_MEDIA_PLAYING)
editor.putLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING)

View File

@ -1,5 +1,11 @@
package ac.mdiq.podcini.preferences
import ac.mdiq.podcini.storage.model.download.ProxyConfig
import ac.mdiq.podcini.storage.model.feed.FeedCounter
import ac.mdiq.podcini.storage.model.feed.FeedPreferences.NewEpisodesAction
import ac.mdiq.podcini.storage.model.feed.SortOrder
import ac.mdiq.podcini.storage.model.feed.SubscriptionsFilter
import ac.mdiq.podcini.storage.model.playback.MediaType
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
@ -9,12 +15,6 @@ import android.view.KeyEvent
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import androidx.preference.PreferenceManager
import ac.mdiq.podcini.storage.model.download.ProxyConfig
import ac.mdiq.podcini.storage.model.feed.FeedCounter
import ac.mdiq.podcini.storage.model.feed.FeedPreferences.NewEpisodesAction
import ac.mdiq.podcini.storage.model.feed.SortOrder
import ac.mdiq.podcini.storage.model.feed.SubscriptionsFilter
import ac.mdiq.podcini.storage.model.playback.MediaType
import org.json.JSONArray
import org.json.JSONException
import java.io.File
@ -847,7 +847,7 @@ object UserPreferences {
// prefs.edit().putString(PREF_INBOX_SORTED_ORDER, "" + sortOrder!!.code).apply()
// }
@JvmStatic
// @JvmStatic
var subscriptionsFilter: SubscriptionsFilter
get() {
val value = prefs.getString(PREF_FILTER_FEED, "")

View File

@ -155,6 +155,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
@Throws(IllegalStateException::class)
fun prepare() {
if (mediaSource == null) return
exoPlayer.setMediaSource(mediaSource!!, false)
exoPlayer.prepare()
}
@ -320,18 +321,12 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
val videoWidth: Int
get() {
if (exoPlayer.videoFormat == null) {
return 0
}
return exoPlayer.videoFormat!!.width
return exoPlayer.videoFormat?.width ?: 0
}
val videoHeight: Int
get() {
if (exoPlayer.videoFormat == null) {
return 0
}
return exoPlayer.videoFormat!!.height
return exoPlayer.videoFormat?.height ?: 0
}
fun setOnBufferingUpdateListener(bufferingUpdateListener: Consumer<Int>?) {

View File

@ -162,22 +162,27 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
callback.onMediaChanged(false)
// TODO: speed
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence)
if (stream) {
if (media!!.getStreamUrl() != null) {
if (playable is FeedMedia && playable.getItem()?.feed?.preferences != null) {
val preferences = playable.getItem()!!.feed!!.preferences!!
mediaPlayer?.setDataSource(
media!!.getStreamUrl()!!,
preferences.username,
preferences.password)
} else {
mediaPlayer?.setDataSource(media!!.getStreamUrl()!!)
when {
stream -> {
val streamurl = media!!.getStreamUrl()
if (streamurl != null) {
if (playable is FeedMedia && playable.getItem()?.feed?.preferences != null) {
val preferences = playable.getItem()!!.feed!!.preferences!!
mediaPlayer?.setDataSource(
streamurl,
preferences.username,
preferences.password)
} else {
mediaPlayer?.setDataSource(streamurl)
}
}
}
} else if (media!!.getLocalMediaUrl() != null && File(media!!.getLocalMediaUrl()!!).canRead()) {
mediaPlayer?.setDataSource(media!!.getLocalMediaUrl()!!)
} else {
throw IOException("Unable to read local file " + media!!.getLocalMediaUrl())
else -> {
val localMediaurl = media!!.getLocalMediaUrl()
if (localMediaurl != null && File(localMediaurl).canRead()) {
mediaPlayer?.setDataSource(localMediaurl)
} else throw IOException("Unable to read local file $localMediaurl")
}
}
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) {

View File

@ -1,39 +1,19 @@
package ac.mdiq.podcini.service.playback
import ac.mdiq.podcini.R
import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.UiModeManager
import android.bluetooth.BluetoothA2dp
import android.content.*
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.media.AudioManager
import android.net.Uri
import android.os.*
import android.service.quicksettings.TileService
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.text.TextUtils
import android.util.Log
import android.util.Pair
import android.view.KeyEvent
import android.view.SurfaceHolder
import android.view.ViewConfiguration
import android.webkit.URLUtil
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.playback.PlayableUtils.saveCurrentPosition
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastPsmp
import ac.mdiq.podcini.playback.cast.CastStateListener
import ac.mdiq.podcini.playback.event.BufferUpdateEvent
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
import ac.mdiq.podcini.playback.event.PlaybackServiceEvent
import ac.mdiq.podcini.playback.event.SleepTimerUpdatedEvent
import ac.mdiq.podcini.preferences.PlaybackPreferences
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.clearCurrentlyPlayingTemporaryPlaybackSpeed
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceFromPreferences
@ -49,38 +29,6 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange
import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.service.QuickSettingsTileService
import ac.mdiq.podcini.service.playback.PlaybackServiceTaskManager.PSTMCallback
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.FeedSearcher
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.util.ChapterUtils.getCurrentChapterIndex
import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded
import ac.mdiq.podcini.util.FeedUtil.shouldAutoDeleteItemsOnThatFeed
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.ui.gui.NotificationUtils
import ac.mdiq.podcini.playback.PlayableUtils.saveCurrentPosition
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.playback.event.BufferUpdateEvent
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
import ac.mdiq.podcini.playback.event.PlaybackServiceEvent
import ac.mdiq.podcini.playback.event.SleepTimerUpdatedEvent
import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent
import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent
import ac.mdiq.podcini.storage.model.feed.*
import ac.mdiq.podcini.storage.model.feed.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastPsmp
import ac.mdiq.podcini.playback.cast.CastStateListener
import ac.mdiq.podcini.preferences.UserPreferences.allEpisodesSortOrder
import ac.mdiq.podcini.preferences.UserPreferences.downloadsSortedOrder
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
@ -103,10 +51,61 @@ import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotifica
import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.service.download.NewEpisodesNotification
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.service.QuickSettingsTileService
import ac.mdiq.podcini.service.playback.PlaybackServiceTaskManager.PSTMCallback
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.FeedSearcher
import ac.mdiq.podcini.storage.model.feed.*
import ac.mdiq.podcini.storage.model.feed.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.appstartintent.MainActivityStarter
import ac.mdiq.podcini.ui.appstartintent.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.gui.NotificationUtils
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.util.ChapterUtils.getCurrentChapterIndex
import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded
import ac.mdiq.podcini.util.FeedUtil.shouldAutoDeleteItemsOnThatFeed
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent
import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent
import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.UiModeManager
import android.bluetooth.BluetoothA2dp
import android.content.*
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.media.AudioManager
import android.net.Uri
import android.os.*
import android.os.Build.VERSION_CODES
import android.service.quicksettings.TileService
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.text.TextUtils
import android.util.Log
import android.util.Pair
import android.view.KeyEvent
import android.view.SurfaceHolder
import android.view.ViewConfiguration
import android.webkit.URLUtil
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media3.common.util.UnstableApi
import io.reactivex.*
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
@ -389,28 +388,35 @@ class PlaybackService : MediaBrowserServiceCompat() {
}
val feedItems: List<FeedItem?>
if (parentId == resources.getString(R.string.queue_label)) {
feedItems = DBReader.getQueue()
} else if (parentId == resources.getString(R.string.downloads_label)) {
feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
FeedItemFilter(FeedItemFilter.DOWNLOADED), downloadsSortedOrder)
} else if (parentId == resources.getString(R.string.episodes_label)) {
feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
FeedItemFilter(FeedItemFilter.UNPLAYED), allEpisodesSortOrder)
} else if (parentId.startsWith("FeedId:")) {
val feedId = parentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toLong()
val feed = DBReader.getFeed(feedId)
feedItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed!!.sortOrder)
} else if (parentId == getString(R.string.current_playing_episode)) {
val playable = createInstanceFromPreferences(this)
if (playable is FeedMedia) {
feedItems = listOf(playable.getItem())
} else {
when {
parentId == resources.getString(R.string.queue_label) -> {
feedItems = DBReader.getQueue()
}
parentId == resources.getString(R.string.downloads_label) -> {
feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
FeedItemFilter(FeedItemFilter.DOWNLOADED), downloadsSortedOrder)
}
parentId == resources.getString(R.string.episodes_label) -> {
feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
FeedItemFilter(FeedItemFilter.UNPLAYED), allEpisodesSortOrder)
}
parentId.startsWith("FeedId:") -> {
val feedId = parentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toLong()
val feed = DBReader.getFeed(feedId)
feedItems = if (feed != null) DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed.sortOrder) else listOf()
}
parentId == getString(R.string.current_playing_episode) -> {
val playable = createInstanceFromPreferences(this)
if (playable is FeedMedia) {
feedItems = listOf(playable.getItem())
} else {
return null
}
}
else -> {
Log.e(TAG, "Parent ID not found: $parentId")
return null
}
} else {
Log.e(TAG, "Parent ID not found: $parentId")
return null
}
var count = 0
for (feedItem in feedItems) {
@ -510,7 +516,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
return
}
val preferences = playable.getItem()!!.feed?.preferences
val preferences = playable.getItem()?.feed?.preferences
val skipIntro = preferences?.feedSkipIntro ?: 0
val context = applicationContext
@ -725,12 +731,12 @@ class PlaybackService : MediaBrowserServiceCompat() {
}
private fun startPlaying(playable: Playable?, allowStreamThisTime: Boolean) {
val localFeed = URLUtil.isContentUrl(playable!!.getStreamUrl())
if (playable == null) return
val localFeed = URLUtil.isContentUrl(playable.getStreamUrl())
val stream = !playable.localFileAvailable() || localFeed
if (stream && !localFeed && !isStreamingAllowed && !allowStreamThisTime) {
displayStreamingNotAllowedNotification(
PlaybackServiceStarter(this, playable)
.intent)
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent)
writeNoMediaPlaying()
stateManager.stopService()
return
@ -784,60 +790,63 @@ class PlaybackService : MediaBrowserServiceCompat() {
override fun statusChanged(newInfo: PSMPInfo?) {
currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN
updateMediaSession(newInfo!!.playerStatus)
when (newInfo.playerStatus) {
PlayerStatus.INITIALIZED -> {
if (mediaPlayer != null) {
writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus)
updateMediaSession(newInfo?.playerStatus)
if (newInfo != null) {
when (newInfo.playerStatus) {
PlayerStatus.INITIALIZED -> {
if (mediaPlayer != null) {
writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus)
}
updateNotificationAndMediaSession(newInfo.playable)
}
updateNotificationAndMediaSession(newInfo.playable)
}
PlayerStatus.PREPARED -> {
if (mediaPlayer != null) {
writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus)
PlayerStatus.PREPARED -> {
if (mediaPlayer != null) {
writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus)
}
taskManager.startChapterLoader(newInfo.playable!!)
}
taskManager.startChapterLoader(newInfo.playable!!)
}
PlayerStatus.PAUSED -> {
updateNotificationAndMediaSession(newInfo.playable)
if (!isCasting) {
stateManager.stopForeground(!isPersistNotify)
}
cancelPositionObserver()
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
}
PlayerStatus.STOPPED -> {}
PlayerStatus.PLAYING -> {
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
saveCurrentPosition(true, null, Playable.INVALID_TIME)
recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(newInfo.playable)
setupPositionObserver()
stateManager.validStartCommandWasReceived()
stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
// set sleep timer if auto-enabled
var autoEnableByTime = true
val fromSetting = autoEnableFrom()
val toSetting = autoEnableTo()
if (fromSetting != toSetting) {
val now: Calendar = GregorianCalendar()
now.timeInMillis = System.currentTimeMillis()
val currentHour = now[Calendar.HOUR_OF_DAY]
autoEnableByTime = isInTimeRange(fromSetting, toSetting, currentHour)
PlayerStatus.PAUSED -> {
updateNotificationAndMediaSession(newInfo.playable)
if (!isCasting) {
stateManager.stopForeground(!isPersistNotify)
}
cancelPositionObserver()
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
}
PlayerStatus.STOPPED -> {}
PlayerStatus.PLAYING -> {
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
saveCurrentPosition(true, null, Playable.INVALID_TIME)
recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(newInfo.playable)
setupPositionObserver()
stateManager.validStartCommandWasReceived()
stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
// set sleep timer if auto-enabled
var autoEnableByTime = true
val fromSetting = autoEnableFrom()
val toSetting = autoEnableTo()
if (fromSetting != toSetting) {
val now: Calendar = GregorianCalendar()
now.timeInMillis = System.currentTimeMillis()
val currentHour = now[Calendar.HOUR_OF_DAY]
autoEnableByTime = isInTimeRange(fromSetting, toSetting, currentHour)
}
if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && autoEnable() && autoEnableByTime && !sleepTimerActive()) {
setSleepTimer(timerMillis())
EventBus.getDefault().post(ac.mdiq.podcini.util.event.MessageEvent(getString(R.string.sleep_timer_enabled_label),
{ disableSleepTimer() }, getString(R.string.undo)))
if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && autoEnable() && autoEnableByTime && !sleepTimerActive()) {
setSleepTimer(timerMillis())
EventBus.getDefault()
.post(ac.mdiq.podcini.util.event.MessageEvent(getString(R.string.sleep_timer_enabled_label),
{ disableSleepTimer() }, getString(R.string.undo)))
}
loadQueueForMediaSession()
}
loadQueueForMediaSession()
PlayerStatus.ERROR -> {
writeNoMediaPlaying()
stateManager.stopService()
}
else -> {}
}
PlayerStatus.ERROR -> {
writeNoMediaPlaying()
stateManager.stopService()
}
else -> {}
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
TileService.requestListeningState(applicationContext,
@ -1059,7 +1068,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
}
var autoSkipped = false
if (autoSkippedFeedMediaId != null && autoSkippedFeedMediaId == item!!.identifyingValue) {
if (autoSkippedFeedMediaId != null && autoSkippedFeedMediaId == item?.identifyingValue) {
autoSkippedFeedMediaId = null
autoSkipped = true
}
@ -1136,7 +1145,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
toast.show()
this.autoSkippedFeedMediaId = feedMedia.getItem()!!.identifyingValue
mediaPlayer!!.skip()
mediaPlayer?.skip()
}
}
@ -1201,7 +1210,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
}
if (showNextChapterOnFullNotification()) {
if (playable != null && playable!!.getChapters().isNotEmpty()) {
if (!playable?.getChapters().isNullOrEmpty()) {
sessionState.addCustomAction(
PlaybackStateCompat.CustomAction.Builder(
CUSTOM_ACTION_NEXT_CHAPTER,
@ -1220,9 +1229,10 @@ class PlaybackService : MediaBrowserServiceCompat() {
)
}
WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!)
mediaSession!!.setPlaybackState(sessionState.build())
if (mediaSession != null) {
WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!)
mediaSession!!.setPlaybackState(sessionState.build())
}
}
private fun updateNotificationAndMediaSession(p: Playable?) {
@ -1252,9 +1262,9 @@ class PlaybackService : MediaBrowserServiceCompat() {
val m = p
if (m.getItem() != null) {
val item = m.getItem()
if (item!!.imageUrl != null) {
if (item?.imageUrl != null) {
iconUri = item.imageUrl
} else if (item.feed != null) {
} else if (item?.feed != null) {
iconUri = item.feed!!.imageUrl
}
}
@ -1289,9 +1299,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
@Synchronized
private fun setupNotification(playable: Playable?) {
Log.d(TAG, "setupNotification")
if (playableIconLoaderThread != null) {
playableIconLoaderThread!!.interrupt()
}
playableIconLoaderThread?.interrupt()
if (playable == null || mediaPlayer == null) {
Log.d(TAG, "setupNotification: playable=$playable")
Log.d(TAG, "setupNotification: mediaPlayer=$mediaPlayer")
@ -1303,7 +1312,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
val playerStatus = mediaPlayer!!.playerStatus
notificationBuilder.setPlayable(playable)
notificationBuilder.setMediaSessionToken(mediaSession!!.sessionToken)
if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.sessionToken)
notificationBuilder.playerStatus = playerStatus
notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed)
@ -1332,7 +1341,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
updateMediaSessionMetadata(playable)
}
}
playableIconLoaderThread!!.start()
playableIconLoaderThread?.start()
}
}
@ -1358,7 +1367,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
duration = playable?.getDuration() ?: Playable.INVALID_TIME
}
if (position != Playable.INVALID_TIME && duration != Playable.INVALID_TIME && playable != null) {
Log.d(TAG, "Saving current position to $position")
Log.d(TAG, "Saving current position to $position $duration")
saveCurrentPosition(playable, position, System.currentTimeMillis())
}
}
@ -1373,11 +1382,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) {
var isPlaying = false
if (info!!.playerStatus == PlayerStatus.PLAYING) {
if (info?.playerStatus == PlayerStatus.PLAYING) {
isPlaying = true
}
if (info.playable != null) {
if (info?.playable != null) {
val i = Intent(whatChanged)
i.putExtra("id", 1L)
i.putExtra("artist", "")
@ -1476,7 +1485,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
Log.d(TAG, "pauseIfPauseOnDisconnect()")
transientPause = (mediaPlayer?.playerStatus == PlayerStatus.PLAYING)
if (isPauseOnHeadsetDisconnect && !isCasting) {
mediaPlayer!!.pause(!isPersistNotify, false)
mediaPlayer?.pause(!isPersistNotify, false)
}
}
@ -1598,7 +1607,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
var isStartWhenPrepared: Boolean
get() = mediaPlayer?.isStartWhenPrepared() ?: false
set(s) {
if (mediaPlayer != null) mediaPlayer!!.setStartWhenPrepared(s)
mediaPlayer?.setStartWhenPrepared(s)
}
fun seekTo(t: Int) {
@ -1646,7 +1655,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
get() = mediaPlayer?.isStreaming() ?: false
val videoSize: Pair<Int, Int>?
get() = mediaPlayer!!.getVideoSize()
get() = mediaPlayer?.getVideoSize()
private fun setupPositionObserver() {
positionEventTimer?.dispose()
@ -1656,8 +1665,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
Log.d(TAG, "notificationBuilder.updatePosition currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed")
EventBus.getDefault().post(PlaybackPositionEvent(currentPosition,
duration))
EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration))
// TODO: why set SDK_INT < 29
if (Build.VERSION.SDK_INT < 29) {
notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed)
@ -1674,7 +1682,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
private fun addPlayableToQueue(playable: Playable?) {
if (playable is FeedMedia) {
val itemId = playable.getItem()!!.id
val itemId = playable.getItem()?.id ?: return
DBWriter.addQueueItem(this, false, true, itemId)
notifyChildrenChanged(getString(R.string.queue_label))
}
@ -1857,9 +1865,6 @@ class PlaybackService : MediaBrowserServiceCompat() {
}
companion object {
/**
* Logging tag
*/
private const val TAG = "PlaybackService"
// TODO: need to experiment this value

View File

@ -65,12 +65,16 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
try {
cachedIcon = Glide.with(context)
.asBitmap()
.load(playable!!.getImageLocation())
.load(playable?.getImageLocation())
.apply(options)
.submit(iconSize, iconSize)
.get()
} catch (e: ExecutionException) {
try {
if (playable == null) {
Log.e(TAG, "playable is null")
return
}
cachedIcon = Glide.with(context)
.asBitmap()
.load(ImageResourceUtils.getFallbackImageLocation(playable!!))

View File

@ -867,7 +867,7 @@ object DBReader {
// getFeedList(adapter)
// TODO:
if (false || subscriptionsFilter != null) {
if (false && subscriptionsFilter != null) {
feeds = subscriptionsFilter.filter(feeds, feedCounters as Map<Long?, Int>).toMutableList()
}

View File

@ -58,9 +58,9 @@ class PodDBAdapter private constructor() {
*
* @return the id of the entry
*/
private fun setFeed(feed: Feed?): Long {
private fun setFeed(feed: Feed): Long {
val values = ContentValues()
values.put(KEY_TITLE, feed!!.feedTitle)
values.put(KEY_TITLE, feed.feedTitle)
values.put(KEY_LINK, feed.link)
values.put(KEY_DESCRIPTION, feed.description)
values.put(KEY_PAYMENT_LINK, FeedFunding.getPaymentLinksAsString(feed.paymentLinks))
@ -77,7 +77,7 @@ class PodDBAdapter private constructor() {
values.put(KEY_IS_PAGED, feed.isPaged)
values.put(KEY_NEXT_PAGE_LINK, feed.nextPageLink)
if (feed.itemFilter != null && feed.itemFilter!!.values.isNotEmpty()) {
if (!feed.itemFilter?.values.isNullOrEmpty()) {
values.put(KEY_HIDE, TextUtils.join(",", feed.itemFilter!!.values))
} else {
values.put(KEY_HIDE, "")
@ -97,8 +97,9 @@ class PodDBAdapter private constructor() {
return feed.id
}
fun setFeedPreferences(prefs: FeedPreferences?) {
require(prefs!!.feedID != 0L) { "Feed ID of preference must not be null" }
fun setFeedPreferences(prefs: FeedPreferences) {
require(prefs.feedID != 0L) { "Feed ID of preference must not be null" }
val values = ContentValues()
values.put(KEY_AUTO_DOWNLOAD_ENABLED, prefs.autoDownload)
values.put(KEY_KEEP_UPDATED, prefs.keepUpdated)
@ -223,7 +224,7 @@ class PodDBAdapter private constructor() {
}
}
if (feed.preferences != null) {
setFeedPreferences(feed.preferences)
setFeedPreferences(feed.preferences!!)
}
}
db.setTransactionSuccessful()
@ -293,10 +294,10 @@ class PodDBAdapter private constructor() {
}
values.put(KEY_PUBDATE, item.getPubDate()!!.time)
values.put(KEY_PAYMENT_LINK, item.paymentLink)
if (saveFeed && item.feed != null) {
setFeed(item.feed)
if (item.feed != null) {
if (saveFeed) setFeed(item.feed!!)
values.put(KEY_FEED, item.feed!!.id)
}
values.put(KEY_FEED, item.feed!!.id)
if (item.isNew) {
values.put(KEY_READ, FeedItem.NEW)
} else if (item.isPlayed()) {
@ -1091,7 +1092,7 @@ class PodDBAdapter private constructor() {
Log.e(TAG, "Database corrupted: " + db.path)
val dbPath = File(db.path)
val backupFolder = context!!.getExternalFilesDir(null)
val backupFolder = context.getExternalFilesDir(null)
val backupFile = File(backupFolder, "CorruptedDatabaseBackup.db")
try {
FileUtils.copyFile(dbPath, backupFile)

View File

@ -74,6 +74,8 @@ class RemoteMedia : Playable {
this.mimeType = item.media!!.mime_type
this.pubDate = item.pubDate
this.description = item.description
this.duration = item.media?.getDuration() ?: 0
}
fun getEpisodeIdentifier(): String? {
@ -159,7 +161,7 @@ class RemoteMedia : Playable {
}
override fun getStreamUrl(): String? {
return downloadUrl
return streamUrl
}
override fun getLocalMediaUrl(): String? {

View File

@ -1,6 +1,5 @@
package ac.mdiq.podcini.ui.activity
//import ac.mdiq.podcini.ui.home.HomeFragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.MainActivityBinding
import ac.mdiq.podcini.net.download.FeedUpdateManager
@ -43,7 +42,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.EditText
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.graphics.Insets

View File

@ -26,7 +26,7 @@ import com.bumptech.glide.request.RequestOptions
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.adapter.FeedItemlistDescriptionAdapter
import ac.mdiq.podcini.ui.adapter.OnlineItemDescriptionAdapter
import ac.mdiq.podcini.feed.FeedUrlNotFoundException
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingMediaType
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying
@ -103,29 +103,33 @@ class OnlineFeedViewActivity : AppCompatActivity() {
private var parser: Disposable? = null
private var updater: Disposable? = null
private lateinit var headerBinding: OnlinefeedviewHeaderBinding
private lateinit var viewBinding: OnlinefeedviewActivityBinding
private lateinit var hBinding: OnlinefeedviewHeaderBinding
private lateinit var binding: OnlinefeedviewActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getTranslucentTheme(this))
super.onCreate(savedInstanceState)
viewBinding = OnlinefeedviewActivityBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
binding = OnlinefeedviewActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
viewBinding.transparentBackground.setOnClickListener { finish() }
viewBinding.closeButton.setOnClickListener { finish() }
viewBinding.card.setOnClickListener(null)
viewBinding.card.setCardBackgroundColor(getColorFromAttr(this, R.attr.colorSurface))
headerBinding = OnlinefeedviewHeaderBinding.inflate(layoutInflater)
binding.transparentBackground.setOnClickListener { finish() }
binding.closeButton.setOnClickListener { finish() }
binding.card.setOnClickListener(null)
binding.card.setCardBackgroundColor(getColorFromAttr(this, R.attr.colorSurface))
hBinding = OnlinefeedviewHeaderBinding.inflate(layoutInflater)
var feedUrl: String? = null
if (intent.hasExtra(ARG_FEEDURL)) {
feedUrl = intent.getStringExtra(ARG_FEEDURL)
} else if (TextUtils.equals(intent.action, Intent.ACTION_SEND)) {
feedUrl = intent.getStringExtra(Intent.EXTRA_TEXT)
} else if (TextUtils.equals(intent.action, Intent.ACTION_VIEW)) {
feedUrl = intent.dataString
when {
intent.hasExtra(ARG_FEEDURL) -> {
feedUrl = intent.getStringExtra(ARG_FEEDURL)
}
TextUtils.equals(intent.action, Intent.ACTION_SEND) -> {
feedUrl = intent.getStringExtra(Intent.EXTRA_TEXT)
}
TextUtils.equals(intent.action, Intent.ACTION_VIEW) -> {
feedUrl = intent.dataString
}
}
if (feedUrl == null) {
@ -164,8 +168,8 @@ class OnlineFeedViewActivity : AppCompatActivity() {
* Displays a progress indicator.
*/
private fun setLoadingLayout() {
viewBinding.progressBar.visibility = View.VISIBLE
viewBinding.feedDisplayContainer.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
binding.feedDisplayContainer.visibility = View.GONE
}
override fun onStart() {
@ -388,18 +392,18 @@ class OnlineFeedViewActivity : AppCompatActivity() {
* This method is executed on the GUI thread.
*/
@UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map<String, String>) {
viewBinding.progressBar.visibility = View.GONE
viewBinding.feedDisplayContainer.visibility = View.VISIBLE
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(viewBinding.root, resId, Snackbar.LENGTH_LONG).show()
Snackbar.make(binding.root, resId, Snackbar.LENGTH_LONG).show()
}
viewBinding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
binding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
viewBinding.listView.addHeaderView(headerBinding.root)
viewBinding.listView.setSelector(android.R.color.transparent)
viewBinding.listView.adapter = FeedItemlistDescriptionAdapter(this, 0, feed.items)
binding.listView.addHeaderView(hBinding.root)
binding.listView.setSelector(android.R.color.transparent)
binding.listView.adapter = OnlineItemDescriptionAdapter(this, 0, feed.items)
if (StringUtils.isNotBlank(feed.imageUrl)) {
Glide.with(this)
@ -409,7 +413,7 @@ class OnlineFeedViewActivity : AppCompatActivity() {
.error(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(viewBinding.coverImage)
.into(binding.coverImage)
Glide.with(this)
.load(feed.imageUrl)
.apply(RequestOptions()
@ -417,14 +421,14 @@ class OnlineFeedViewActivity : AppCompatActivity() {
.error(R.color.image_readability_tint)
.transform(FastBlurTransformation())
.dontAnimate())
.into(viewBinding.backgroundImage)
.into(binding.backgroundImage)
}
viewBinding.titleLabel.text = feed.title
viewBinding.authorLabel.text = feed.author
headerBinding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"")
binding.titleLabel.text = feed.title
binding.authorLabel.text = feed.author
hBinding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"")
viewBinding.subscribeButton.setOnClickListener {
binding.subscribeButton.setOnClickListener {
if (feedInFeedlist()) {
openFeed()
} else {
@ -434,29 +438,29 @@ class OnlineFeedViewActivity : AppCompatActivity() {
}
}
viewBinding.stopPreviewButton.setOnClickListener {
binding.stopPreviewButton.setOnClickListener {
writeNoMediaPlaying()
sendLocalBroadcast(this, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)
}
if (isEnableAutodownload) {
val preferences = getSharedPreferences(PREFS, MODE_PRIVATE)
viewBinding.autoDownloadCheckBox.isChecked = preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
binding.autoDownloadCheckBox.isChecked = preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
}
headerBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED
headerBinding.txtvDescription.setOnClickListener {
if (headerBinding.txtvDescription.maxLines > DESCRIPTION_MAX_LINES_COLLAPSED) {
headerBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED
hBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED
hBinding.txtvDescription.setOnClickListener {
if (hBinding.txtvDescription.maxLines > DESCRIPTION_MAX_LINES_COLLAPSED) {
hBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED
} else {
headerBinding.txtvDescription.maxLines = 2000
hBinding.txtvDescription.maxLines = 2000
}
}
if (alternateFeedUrls.isEmpty()) {
viewBinding.alternateUrlsSpinner.visibility = View.GONE
binding.alternateUrlsSpinner.visibility = View.GONE
} else {
viewBinding.alternateUrlsSpinner.visibility = View.VISIBLE
binding.alternateUrlsSpinner.visibility = View.VISIBLE
val alternateUrlsList: MutableList<String> = ArrayList()
val alternateUrlsTitleList: MutableList<String?> = ArrayList()
@ -479,8 +483,8 @@ class OnlineFeedViewActivity : AppCompatActivity() {
}
adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item)
viewBinding.alternateUrlsSpinner.adapter = adapter
viewBinding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
binding.alternateUrlsSpinner.adapter = adapter
binding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
selectedDownloadUrl = alternateUrlsList[position]
}
@ -507,11 +511,11 @@ class OnlineFeedViewActivity : AppCompatActivity() {
if (dli == null || selectedDownloadUrl == null) return
if (dli.isDownloadingEpisode(selectedDownloadUrl!!)) {
viewBinding.subscribeButton.isEnabled = false
viewBinding.subscribeButton.setText(R.string.subscribing_label)
binding.subscribeButton.isEnabled = false
binding.subscribeButton.setText(R.string.subscribing_label)
} else if (feedInFeedlist()) {
viewBinding.subscribeButton.isEnabled = true
viewBinding.subscribeButton.setText(R.string.open_podcast)
binding.subscribeButton.isEnabled = true
binding.subscribeButton.setText(R.string.open_podcast)
if (didPressSubscribe) {
didPressSubscribe = false
@ -519,7 +523,7 @@ class OnlineFeedViewActivity : AppCompatActivity() {
val feedPreferences = feed1.preferences
if (feedPreferences != null) {
if (isEnableAutodownload) {
val autoDownload = viewBinding.autoDownloadCheckBox.isChecked
val autoDownload = binding.autoDownloadCheckBox.isChecked
feedPreferences.autoDownload = autoDownload
val preferences = getSharedPreferences(PREFS, MODE_PRIVATE)
@ -536,10 +540,10 @@ class OnlineFeedViewActivity : AppCompatActivity() {
openFeed()
}
} else {
viewBinding.subscribeButton.isEnabled = true
viewBinding.subscribeButton.setText(R.string.subscribe_label)
binding.subscribeButton.isEnabled = true
binding.subscribeButton.setText(R.string.subscribe_label)
if (isEnableAutodownload) {
viewBinding.autoDownloadCheckBox.visibility = View.VISIBLE
binding.autoDownloadCheckBox.visibility = View.VISIBLE
}
}
}
@ -617,7 +621,7 @@ class OnlineFeedViewActivity : AppCompatActivity() {
@Subscribe(threadMode = ThreadMode.MAIN)
fun playbackStateChanged(event: PlayerStatusEvent?) {
val isPlayingPreview = currentlyPlayingMediaType == RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA.toLong()
viewBinding.stopPreviewButton.visibility = if (isPlayingPreview) View.VISIBLE else View.GONE
binding.stopPreviewButton.visibility = if (isPlayingPreview) View.VISIBLE else View.GONE
}
/**

View File

@ -1,5 +1,8 @@
package ac.mdiq.podcini.ui.adapter.itunes
package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.OnlinePodcastListitemBinding
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
import ac.mdiq.podcini.ui.activity.MainActivity
import android.content.Context
import android.view.View
@ -7,40 +10,27 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ItunesPodcastListitemBinding
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
import androidx.media3.common.util.UnstableApi
class ItunesAdapter(
/**
* Related Context
*/
private val context: Context, objects: List<PodcastSearchResult>
) : ArrayAdapter<PodcastSearchResult?>(context, 0, objects) {
/**
* List holding the podcasts found in the search
*/
class OnlineFeedsAdapter(private val context: Context, objects: List<PodcastSearchResult>) :
ArrayAdapter<PodcastSearchResult?>(context, 0, objects) {
// List holding the podcasts found in the search
private val data: List<PodcastSearchResult> = objects
@UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//Current podcast
val podcast: PodcastSearchResult = data[position]
//ViewHolder
val viewHolder: PodcastViewHolder
//Resulting view
val view: View
//Handle view holder stuff
if (convertView == null) {
view = (context as MainActivity).layoutInflater.inflate(R.layout.itunes_podcast_listitem, parent, false)
view = (context as MainActivity).layoutInflater.inflate(R.layout.online_podcast_listitem, parent, false)
viewHolder = PodcastViewHolder(view)
view.tag = viewHolder
} else {
@ -50,15 +40,28 @@ class ItunesAdapter(
// Set the title
viewHolder.titleView.text = podcast.title
if (podcast.author != null && podcast.author.trim { it <= ' ' }.isNotEmpty()) {
viewHolder.authorView.text = podcast.author
viewHolder.authorView.visibility = View.VISIBLE
} else if (podcast.feedUrl != null && !podcast.feedUrl.contains("itunes.apple.com")) {
viewHolder.authorView.text = podcast.feedUrl
viewHolder.authorView.visibility = View.VISIBLE
} else {
viewHolder.authorView.visibility = View.GONE
when {
!podcast.author.isNullOrBlank() -> {
viewHolder.authorView.text = podcast.author.trim { it <= ' ' }
viewHolder.authorView.visibility = View.VISIBLE
}
podcast.feedUrl != null && !podcast.feedUrl.contains("itunes.apple.com") -> {
viewHolder.authorView.text = podcast.feedUrl
viewHolder.authorView.visibility = View.VISIBLE
}
else -> {
viewHolder.authorView.visibility = View.INVISIBLE
}
}
viewHolder.source.text = podcast.source + ": " + podcast.feedUrl
if (podcast.count != null) {
viewHolder.countView.text = podcast.count.toString() + " episodes"
viewHolder.countView.visibility = View.VISIBLE
} else viewHolder.countView.visibility = View.INVISIBLE
if (podcast.update != null) {
viewHolder.updateView.text = podcast.update
viewHolder.updateView.visibility = View.VISIBLE
} else viewHolder.updateView.visibility = View.INVISIBLE
//Update the empty imageView with the image from the feed
Glide.with(context)
@ -71,25 +74,22 @@ class ItunesAdapter(
.dontAnimate())
.into(viewHolder.coverView)
//Feed the grid view
return view
}
/**
* View holder object for the GridView
*/
internal class PodcastViewHolder(view: View) {
val binding = ItunesPodcastListitemBinding.bind(view)
/**
* ImageView holding the Podcast image
*/
val binding = OnlinePodcastListitemBinding.bind(view)
val coverView: ImageView = binding.imgvCover
/**
* TextView holding the Podcast title
*/
val titleView: TextView = binding.txtvTitle
val authorView: TextView = binding.txtvAuthor
val countView: TextView = binding.count
val updateView: TextView = binding.update
val source: TextView = binding.source
}
}

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ItemdescriptionListitemBinding
import ac.mdiq.podcini.databinding.OnlineItemDescriptionBinding
import ac.mdiq.podcini.service.playback.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.util.DateFormatter.formatAbbrev
import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed
@ -24,8 +24,8 @@ import androidx.media3.common.util.UnstableApi
/**
* List adapter for showing a list of FeedItems with their title and description.
*/
class FeedItemlistDescriptionAdapter(context: Context, resource: Int, objects: List<FeedItem?>?) :
ArrayAdapter<FeedItem?>(context, resource, objects!!) {
class OnlineItemDescriptionAdapter(context: Context, resource: Int, items: List<FeedItem>) :
ArrayAdapter<FeedItem>(context, resource, items) {
@UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var convertView = convertView
val holder: Holder
@ -36,8 +36,8 @@ class FeedItemlistDescriptionAdapter(context: Context, resource: Int, objects: L
if (convertView == null) {
holder = Holder()
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false)
val binding = ItemdescriptionListitemBinding.bind(convertView)
convertView = inflater.inflate(R.layout.online_item_description, parent, false)
val binding = OnlineItemDescriptionBinding.bind(convertView)
holder.title = binding.txtvTitle
holder.pubDate = binding.txtvPubDate
holder.description = binding.txtvDescription
@ -48,22 +48,21 @@ class FeedItemlistDescriptionAdapter(context: Context, resource: Int, objects: L
holder = convertView.tag as Holder
}
holder.title!!.text = item!!.title
holder.pubDate!!.text = formatAbbrev(context, item.pubDate)
holder.title.text = item!!.title
holder.pubDate.text = formatAbbrev(context, item.pubDate)
if (item.description != null) {
val description = HtmlToPlainText.getPlainText(item.description!!)
.replace("\n".toRegex(), " ")
.replace("\\s+".toRegex(), " ")
.trim { it <= ' ' }
holder.description!!.text = description
holder.description!!.maxLines = MAX_LINES_COLLAPSED
holder.description.text = description
holder.description.maxLines = MAX_LINES_COLLAPSED
}
holder.description!!.tag = false
holder.preview!!.visibility = View.GONE
holder.preview!!.setOnClickListener {
if (item.media == null) {
return@setOnClickListener
}
holder.description.tag = false
holder.preview.visibility = View.GONE
holder.preview.setOnClickListener {
if (item.media == null) return@setOnClickListener
val playable: Playable = RemoteMedia(item)
if (!isStreamingAllowed) {
StreamingConfirmationDialog(context, playable).show()
@ -78,26 +77,26 @@ class FeedItemlistDescriptionAdapter(context: Context, resource: Int, objects: L
}
}
convertView!!.setOnClickListener {
if (holder.description!!.tag == true) {
holder.description!!.maxLines = MAX_LINES_COLLAPSED
holder.preview!!.visibility = View.GONE
holder.description!!.tag = false
if (holder.description.tag == true) {
holder.description.maxLines = MAX_LINES_COLLAPSED
holder.preview.visibility = View.GONE
holder.description.tag = false
} else {
holder.description!!.maxLines = 30
holder.description!!.tag = true
holder.description.maxLines = 30
holder.description.tag = true
holder.preview!!.visibility = if (item.media != null) View.VISIBLE else View.GONE
holder.preview!!.setText(R.string.preview_episode)
holder.preview.visibility = if (item.media != null) View.VISIBLE else View.GONE
holder.preview.setText(R.string.preview_episode)
}
}
return convertView
}
internal class Holder {
var title: TextView? = null
var pubDate: TextView? = null
var description: TextView? = null
var preview: Button? = null
lateinit var title: TextView
lateinit var pubDate: TextView
lateinit var description: TextView
lateinit var preview: Button
}
companion object {

View File

@ -178,13 +178,14 @@ open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) :
title.text = drawerItem.title
producer.text = drawerItem.producer
coverImage.contentDescription = drawerItem.title
if (drawerItem.counter > 0) {
// TODO: need to use more specific number
count.text = NumberFormat.getInstance().format(drawerItem.counter.toLong()) + " episodes"
// count.text = NumberFormat.getInstance().format(drawerItem.feed.items.size.toLong()) + " episodes"
count.visibility = View.VISIBLE
} else {
count.visibility = View.GONE
count.visibility = View.VISIBLE
}
val mainActRef = mainActivityRef.get() ?: return

View File

@ -18,6 +18,8 @@ import ac.mdiq.podcini.databinding.FilterDialogBinding
import ac.mdiq.podcini.feed.FeedItemFilterGroup
import ac.mdiq.podcini.databinding.FilterDialogRowBinding
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.ui.fragment.ItemDescriptionFragment
import android.util.Log
abstract class ItemFilterDialog : BottomSheetDialogFragment() {
private lateinit var rows: LinearLayout
@ -27,6 +29,7 @@ abstract class ItemFilterDialog : BottomSheetDialogFragment() {
val binding = FilterDialogBinding.bind(layout)
rows = binding.filterRows
val filter = requireArguments().getSerializable(ARGUMENT_FILTER) as FeedItemFilter?
Log.d("ItemFilterDialog", "fragment onCreateView")
//add filter rows
for (item in FeedItemFilterGroup.entries) {

View File

@ -18,19 +18,19 @@ import ac.mdiq.podcini.databinding.SortDialogItemBinding
import ac.mdiq.podcini.storage.model.feed.SortOrder
open class ItemSortDialog : BottomSheetDialogFragment() {
protected lateinit var viewBinding: SortDialogBinding
protected lateinit var binding: SortDialogBinding
protected var sortOrder: SortOrder? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewBinding = SortDialogBinding.inflate(inflater)
binding = SortDialogBinding.inflate(inflater)
populateList()
viewBinding.keepSortedCheckbox.setOnCheckedChangeListener { _: CompoundButton?, _: Boolean -> this@ItemSortDialog.onSelectionChanged() }
return viewBinding.root
binding.keepSortedCheckbox.setOnCheckedChangeListener { _: CompoundButton?, _: Boolean -> this@ItemSortDialog.onSelectionChanged() }
return binding.root
}
private fun populateList() {
viewBinding.gridLayout.removeAllViews()
binding.gridLayout.removeAllViews()
onAddItem(R.string.episode_title, SortOrder.EPISODE_TITLE_A_Z, SortOrder.EPISODE_TITLE_Z_A, true)
onAddItem(R.string.feed_title, SortOrder.FEED_TITLE_A_Z, SortOrder.FEED_TITLE_Z_A, true)
onAddItem(R.string.duration, SortOrder.DURATION_SHORT_LONG, SortOrder.DURATION_LONG_SHORT, true)
@ -43,7 +43,7 @@ open class ItemSortDialog : BottomSheetDialogFragment() {
protected open fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) {
if (sortOrder == ascending || sortOrder == descending) {
val item = SortDialogItemActiveBinding.inflate(layoutInflater, viewBinding.gridLayout, false)
val item = SortDialogItemActiveBinding.inflate(layoutInflater, binding.gridLayout, false)
val other: SortOrder
when {
ascending == descending -> {
@ -64,17 +64,17 @@ open class ItemSortDialog : BottomSheetDialogFragment() {
populateList()
onSelectionChanged()
}
viewBinding.gridLayout.addView(item.root)
binding.gridLayout.addView(item.root)
} else {
val item = SortDialogItemBinding.inflate(
layoutInflater, viewBinding.gridLayout, false)
layoutInflater, binding.gridLayout, false)
item.button.setText(title)
item.button.setOnClickListener {
sortOrder = if (ascendingIsDefault) ascending else descending
populateList()
onSelectionChanged()
}
viewBinding.gridLayout.addView(item.root)
binding.gridLayout.addView(item.root)
}
}

View File

@ -68,22 +68,22 @@ class SwipeActionsDialog(private val context: Context, private val tag: String)
}
builder.setTitle(context.getString(R.string.swipeactions_label) + " - " + forFragment)
val viewBinding = SwipeactionsDialogBinding.inflate(LayoutInflater.from(context))
builder.setView(viewBinding.root)
val binding = SwipeactionsDialogBinding.inflate(LayoutInflater.from(context))
builder.setView(binding.root)
viewBinding.enableSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
viewBinding.actionLeftContainer.root.alpha = if (b) 1.0f else 0.4f
viewBinding.actionRightContainer.root.alpha = if (b) 1.0f else 0.4f
binding.enableSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
binding.actionLeftContainer.root.alpha = if (b) 1.0f else 0.4f
binding.actionRightContainer.root.alpha = if (b) 1.0f else 0.4f
}
viewBinding.enableSwitch.isChecked = isSwipeActionEnabled(context, tag)
binding.enableSwitch.isChecked = isSwipeActionEnabled(context, tag)
setupSwipeDirectionView(viewBinding.actionLeftContainer, LEFT)
setupSwipeDirectionView(viewBinding.actionRightContainer, RIGHT)
setupSwipeDirectionView(binding.actionLeftContainer, LEFT)
setupSwipeDirectionView(binding.actionRightContainer, RIGHT)
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
savePrefs(tag, rightAction!!.getId(), leftAction!!.getId())
saveActionsEnabledPrefs(viewBinding.enableSwitch.isChecked)
saveActionsEnabledPrefs(binding.enableSwitch.isChecked)
prefsChanged.onCall()
}

View File

@ -227,9 +227,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) {
if (controller == null) {
return
}
if (controller == null) return
updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
}
@ -243,13 +241,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private fun setupLengthTextView() {
showTimeLeft = UserPreferences.shouldShowRemainingTime()
txtvLength.setOnClickListener(View.OnClickListener {
if (controller == null) {
return@OnClickListener
}
if (controller == null) return@OnClickListener
showTimeLeft = !showTimeLeft
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
updatePosition(PlaybackPositionEvent(controller!!.position,
controller!!.duration))
updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
})
}
@ -302,9 +298,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private fun updateUi(media: Playable?) {
if (controller != null) duration = controller!!.duration
if (media == null) {
return
}
if (media == null) return
episodeTitle.text = media.getEpisodeTitle()
updatePosition(PlaybackPositionEvent(media.getPosition(), media.getDuration()))
updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media)))
@ -340,14 +335,19 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
@Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused")
fun bufferUpdate(event: BufferUpdateEvent) {
if (event.hasStarted()) {
progressIndicator.visibility = View.VISIBLE
} else if (event.hasEnded()) {
progressIndicator.visibility = View.GONE
} else if (controller != null && controller!!.isStreaming) {
sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt())
} else {
sbPosition.setSecondaryProgress(0)
when {
event.hasStarted() -> {
progressIndicator.visibility = View.VISIBLE
}
event.hasEnded() -> {
progressIndicator.visibility = View.GONE
}
controller != null && controller!!.isStreaming -> {
sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt())
}
else -> {
sbPosition.setSecondaryProgress(0)
}
}
}
@ -363,7 +363,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
currentChapterIndex = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), currentPosition)
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) {
Log.w(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 $currentPosition $duration")
return
}
txtvPosition.text = Converter.getDurationStringLong(currentPosition)
@ -373,14 +373,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
if (showTimeLeft) {
txtvLength.setContentDescription(getString(R.string.remaining_time,
Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong())))
txtvLength.text = (if ((remainingTime > 0)) "-" else "") + Converter.getDurationStringLong(remainingTime)
txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime)
} else {
txtvLength.setContentDescription(getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
txtvLength.text = Converter.getDurationStringLong(duration)
}
if (!sbPosition.isPressed) {
if (!sbPosition.isPressed && event.duration > 0) {
val progress: Float = (event.position.toFloat()) / event.duration
sbPosition.progress = (progress * sbPosition.max).toInt()
}
@ -508,7 +508,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
Log.d(TAG, "getItem($position)")
return when (position) {
// TODO: cover page is not very useful
FIRST_PAGE -> ItemDescriptionFragment()
SECOND_PAGE -> CoverFragment()
else -> ItemDescriptionFragment()

View File

@ -1,48 +1,47 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.ui.activity.MainActivity
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.ProgressBar
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.snackbar.Snackbar
import com.leinardi.android.speeddial.SpeedDialActionItem
import com.leinardi.android.speeddial.SpeedDialView
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.ui.adapter.EpisodeItemListAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.adapter.actionbutton.DeleteActionButton
import ac.mdiq.podcini.ui.menuhandler.MenuItemUtils
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.util.FeedItemUtil
import ac.mdiq.podcini.net.download.FeedUpdateManager
import ac.mdiq.podcini.ui.dialog.ItemSortDialog
import ac.mdiq.podcini.util.event.EpisodeDownloadEvent
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.playback.event.PlaybackPositionEvent
import ac.mdiq.podcini.ui.fragment.actions.EpisodeMultiSelectActionHandler
import ac.mdiq.podcini.ui.fragment.swipeactions.SwipeActions
import ac.mdiq.podcini.ui.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.DBReader
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.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodeItemListAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.adapter.actionbutton.DeleteActionButton
import ac.mdiq.podcini.ui.dialog.ItemSortDialog
import ac.mdiq.podcini.ui.fragment.actions.EpisodeMultiSelectActionHandler
import ac.mdiq.podcini.ui.fragment.swipeactions.SwipeActions
import ac.mdiq.podcini.ui.menuhandler.FeedItemMenuHandler
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.Converter
import ac.mdiq.podcini.util.FeedItemUtil
import ac.mdiq.podcini.util.event.EpisodeDownloadEvent
import ac.mdiq.podcini.util.event.FeedItemEvent
import ac.mdiq.podcini.util.event.SwipeActionsChangedEvent
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.snackbar.Snackbar
import com.leinardi.android.speeddial.SpeedDialActionItem
import com.leinardi.android.speeddial.SpeedDialView
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@ -51,8 +50,6 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
/**
* Displays all completed downloads and provides a button to delete them.
@ -61,6 +58,7 @@ class CompletedDownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeLis
private var runningDownloads: Set<String>? = HashSet()
private var items: MutableList<FeedItem> = mutableListOf()
private lateinit var binding: SimpleListFragmentBinding
private lateinit var infoBar: TextView
private lateinit var adapter: CompletedDownloadsListAdapter
private lateinit var toolbar: MaterialToolbar
@ -77,7 +75,7 @@ class CompletedDownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeLis
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = SimpleListFragmentBinding.inflate(inflater)
binding = SimpleListFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
toolbar = binding.toolbar
@ -101,9 +99,17 @@ class CompletedDownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeLis
adapter.setOnSelectModeListener(this)
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
swipeActions = SwipeActions(this, TAG).attachTo(recyclerView)
swipeActions.setFilter(FeedItemFilter(FeedItemFilter.DOWNLOADED))
if (swipeActions.actions?.left != null) {
binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
if (animator is SimpleItemAnimator) {
animator.supportsChangeAnimations = false
@ -291,6 +297,16 @@ class CompletedDownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeLis
loadItems()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) {
if (swipeActions.actions?.left != null) {
binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
}
private fun loadItems() {
disposable?.dispose()
@ -337,7 +353,7 @@ class CompletedDownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeLis
for (item in items) {
sizeMB += item.media?.size?:0
}
info += "" + getString(R.string.size) + " : " + (sizeMB / 1000000) + " MB"
info += "" + (sizeMB / 1000000) + " MB"
}
infoBar.text = info
}

View File

@ -8,7 +8,7 @@ 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.adapter.itunes.ItunesAdapter
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
import ac.mdiq.podcini.util.event.DiscoveryDefaultUpdateEvent
import android.content.Context
import android.content.DialogInterface
@ -50,7 +50,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
/**
* Adapter responsible with the search results.
*/
private var adapter: ItunesAdapter? = null
private var adapter: OnlineFeedsAdapter? = null
/**
* List of podcasts retreived from the search.
@ -99,7 +99,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Log.d(TAG, "fragment onCreateView")
gridView = viewBinding.gridView
adapter = ItunesAdapter(requireActivity(), ArrayList())
adapter = OnlineFeedsAdapter(requireActivity(), ArrayList())
gridView.setAdapter(adapter)
toolbar = viewBinding.toolbar

View File

@ -58,6 +58,7 @@ abstract class EpisodesListFragment : Fragment(), SelectableAdapter.OnSelectMode
protected var hasMoreItems: Boolean = false
private var displayUpArrow = false
lateinit var binding: EpisodesListFragmentBinding
lateinit var recyclerView: EpisodeItemListRecyclerView
lateinit var emptyView: EmptyViewHandler
lateinit var speedDialView: SpeedDialView
@ -73,6 +74,122 @@ abstract class EpisodesListFragment : Fragment(), SelectableAdapter.OnSelectMode
protected var disposable: Disposable? = null
protected lateinit var txtvInformation: TextView
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
binding = EpisodesListFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
txtvInformation = binding.txtvInformation
toolbar = binding.toolbar
toolbar.setOnMenuItemClickListener(this)
toolbar.setOnLongClickListener {
recyclerView.scrollToPosition(5)
recyclerView.post { recyclerView.smoothScrollToPosition(0) }
false
}
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
}
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
recyclerView = binding.recyclerView
recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
setupLoadMoreScrollListener()
recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
swipeActions = SwipeActions(this, getFragmentTag()).attachTo(recyclerView)
swipeActions.setFilter(getFilter())
if (swipeActions.actions?.left != null) {
binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
if (animator is SimpleItemAnimator) {
animator.supportsChangeAnimations = false
}
swipeRefreshLayout = binding.swipeRefresh
swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
swipeRefreshLayout.setOnRefreshListener {
FeedUpdateManager.runOnceOrAsk(requireContext())
}
listAdapter = object : EpisodeItemListAdapter(activity as MainActivity) {
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
// if (!inActionMode()) {
// menu.findItem(R.id.multi_select).setVisible(true)
// }
MenuItemUtils.setOnClickListeners(menu
) { item: MenuItem ->
this@EpisodesListFragment.onContextItemSelected(item)
}
}
}
listAdapter.setOnSelectModeListener(this)
recyclerView.adapter = listAdapter
progressBar = binding.progressBar
progressBar.visibility = View.VISIBLE
emptyView = EmptyViewHandler(requireContext())
emptyView.attachToRecyclerView(recyclerView)
emptyView.setIcon(R.drawable.ic_feed)
emptyView.setTitle(R.string.no_all_episodes_head_label)
emptyView.setMessage(R.string.no_all_episodes_label)
emptyView.updateAdapter(listAdapter)
emptyView.hide()
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
speedDialView = multiSelectDial.fabSD
speedDialView.overlayLayout = multiSelectDial.fabSDOverlay
speedDialView.inflate(R.menu.episodes_apply_action_speeddial)
speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener {
override fun onMainActionSelected(): Boolean {
return false
}
override fun onToggleChanged(open: Boolean) {
if (open && listAdapter.selectedCount == 0) {
(activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected,
Snackbar.LENGTH_SHORT)
speedDialView.close()
}
}
})
speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
var confirmationString = 0
if (listAdapter.selectedItems.size >= 25 || listAdapter.shouldSelectLazyLoadedItems()) {
// Should ask for confirmation
if (actionItem.id == R.id.mark_read_batch) {
confirmationString = R.string.multi_select_mark_played_confirmation
} else if (actionItem.id == R.id.mark_unread_batch) {
confirmationString = R.string.multi_select_mark_unplayed_confirmation
}
}
if (confirmationString == 0) {
performMultiSelectAction(actionItem.id)
} else {
object : ConfirmationDialog(activity as MainActivity, R.string.multi_select, confirmationString) {
override fun onConfirmButtonPressed(dialog: DialogInterface) {
performMultiSelectAction(actionItem.id)
}
}.createNewDialog().show()
}
true
}
EventBus.getDefault().register(this)
loadItems()
return binding.root
}
override fun onResume() {
super.onResume()
registerForContextMenu(recyclerView)
@ -126,115 +243,6 @@ abstract class EpisodesListFragment : Fragment(), SelectableAdapter.OnSelectMode
}
}
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
val viewBinding = EpisodesListFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
txtvInformation = viewBinding.txtvInformation
toolbar = viewBinding.toolbar
toolbar.setOnMenuItemClickListener(this)
toolbar.setOnLongClickListener {
recyclerView.scrollToPosition(5)
recyclerView.post { recyclerView.smoothScrollToPosition(0) }
false
}
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) {
displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
}
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
recyclerView = viewBinding.recyclerView
recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
setupLoadMoreScrollListener()
recyclerView.addOnScrollListener(LiftOnScrollListener(viewBinding.appbar))
swipeActions = SwipeActions(this, getFragmentTag()).attachTo(recyclerView)
swipeActions.setFilter(getFilter())
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
if (animator is SimpleItemAnimator) {
animator.supportsChangeAnimations = false
}
swipeRefreshLayout = viewBinding.swipeRefresh
swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
swipeRefreshLayout.setOnRefreshListener {
FeedUpdateManager.runOnceOrAsk(requireContext())
}
listAdapter = object : EpisodeItemListAdapter(activity as MainActivity) {
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
// if (!inActionMode()) {
// menu.findItem(R.id.multi_select).setVisible(true)
// }
MenuItemUtils.setOnClickListeners(menu
) { item: MenuItem ->
this@EpisodesListFragment.onContextItemSelected(item)
}
}
}
listAdapter.setOnSelectModeListener(this)
recyclerView.adapter = listAdapter
progressBar = viewBinding.progressBar
progressBar.visibility = View.VISIBLE
emptyView = EmptyViewHandler(requireContext())
emptyView.attachToRecyclerView(recyclerView)
emptyView.setIcon(R.drawable.ic_feed)
emptyView.setTitle(R.string.no_all_episodes_head_label)
emptyView.setMessage(R.string.no_all_episodes_label)
emptyView.updateAdapter(listAdapter)
emptyView.hide()
val multiSelectDial = MultiSelectSpeedDialBinding.bind(viewBinding.root)
speedDialView = multiSelectDial.fabSD
speedDialView.overlayLayout = multiSelectDial.fabSDOverlay
speedDialView.inflate(R.menu.episodes_apply_action_speeddial)
speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener {
override fun onMainActionSelected(): Boolean {
return false
}
override fun onToggleChanged(open: Boolean) {
if (open && listAdapter.selectedCount == 0) {
(activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected,
Snackbar.LENGTH_SHORT)
speedDialView.close()
}
}
})
speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
var confirmationString = 0
if (listAdapter.selectedItems.size >= 25 || listAdapter.shouldSelectLazyLoadedItems()) {
// Should ask for confirmation
if (actionItem.id == R.id.mark_read_batch) {
confirmationString = R.string.multi_select_mark_played_confirmation
} else if (actionItem.id == R.id.mark_unread_batch) {
confirmationString = R.string.multi_select_mark_unplayed_confirmation
}
}
if (confirmationString == 0) {
performMultiSelectAction(actionItem.id)
} else {
object : ConfirmationDialog(activity as MainActivity, R.string.multi_select, confirmationString) {
override fun onConfirmButtonPressed(dialog: DialogInterface) {
performMultiSelectAction(actionItem.id)
}
}.createNewDialog().show()
}
true
}
EventBus.getDefault().register(this)
loadItems()
return viewBinding.root
}
@UnstableApi private fun performMultiSelectAction(actionItemId: Int) {
val handler = EpisodeMultiSelectActionHandler((activity as MainActivity), actionItemId)
Completable.fromAction {
@ -379,6 +387,16 @@ abstract class EpisodesListFragment : Fragment(), SelectableAdapter.OnSelectMode
loadItems()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) {
if (swipeActions.actions?.left != null) {
binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
}
fun loadItems() {
disposable?.dispose()

View File

@ -240,7 +240,7 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
if (showTimeLeft) {
txtvLength.setContentDescription(getString(R.string.remaining_time,
Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong())))
txtvLength.text = (if ((remainingTime > 0)) "-" else "") + Converter.getDurationStringLong(remainingTime)
txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime)
} else {
txtvLength.setContentDescription(getString(R.string.chapter_duration,
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
@ -289,9 +289,7 @@ class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
}
disposable?.dispose()
disposable = Maybe.fromCallable<Playable?> {
controller?.getMedia()
}
disposable = Maybe.fromCallable<Playable?> { controller?.getMedia() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ media: Playable? -> this.updateUi(media) },

View File

@ -115,7 +115,15 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
adapter = FeedItemListAdapter(activity as MainActivity)
adapter.setOnSelectModeListener(this)
binding.recyclerView.adapter = adapter
swipeActions = SwipeActions(this, TAG).attachTo(binding.recyclerView)
if (swipeActions.actions?.left != null) {
binding.header.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.header.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
binding.progressBar.visibility = View.VISIBLE
val iconTintManager: ToolbarIconTintManager = object : ToolbarIconTintManager(
@ -399,6 +407,16 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) {
if (swipeActions.actions?.left != null) {
binding.header.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.header.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: FeedUpdateRunningEvent) {
nextPageLoader.setLoadingState(event.isFeedUpdateRunning)

View File

@ -33,7 +33,7 @@ class ItemDescriptionFragment : Fragment() {
private var controller: PlaybackController? = null
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Log.d(TAG, "Creating view")
Log.d(TAG, "fragment onCreateView")
val binding = ItemDescriptionFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")

View File

@ -326,9 +326,8 @@ class ItemFragment : Fragment() {
}
@OptIn(UnstableApi::class) private fun openPodcast() {
if (item == null) {
return
}
if (item == null) return
val fragment: Fragment = FeedItemlistFragment.newInstance(item!!.feedId)
(activity as MainActivity).loadChildFragment(fragment)
}

View File

@ -145,9 +145,8 @@ class ItemPageFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
@UnstableApi private fun openPodcast() {
if (item == null) {
return
}
if (item == null) return
val fragment: Fragment = FeedItemlistFragment.newInstance(item!!.feedId)
(activity as MainActivity).loadChildFragment(fragment)
}

View File

@ -162,9 +162,8 @@ class ItemPagerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
@UnstableApi private fun openPodcast() {
if (item == null) {
return
}
if (item == null) return
val fragment: Fragment = FeedItemlistFragment.newInstance(item!!.feedId)
(activity as MainActivity).loadChildFragment(fragment)
}

View File

@ -65,7 +65,6 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange
): View {
super.onCreateView(inflater, container, savedInstanceState)
val binding = NavListBinding.inflate(inflater)
// val root: View = inflater.inflate(R.layout.nav_list, container, false)
Log.d(TAG, "fragment onCreateView")
setupDrawerRoundBackground(binding.root)

View File

@ -18,21 +18,17 @@ 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.itunes.ItunesAdapter
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
/**
* Constructor
*/
: Fragment() {
class OnlineSearchFragment : Fragment() {
/**
* Adapter responsible with the search results
*/
private var adapter: ItunesAdapter? = null
private var adapter: OnlineFeedsAdapter? = null
private var searchProvider: PodcastSearcher? = null
private lateinit var gridView: GridView
@ -63,13 +59,11 @@ class OnlineSearchFragment
}
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// Inflate the layout for this fragment
val binding = FragmentItunesSearchBinding.inflate(inflater)
// val root: View = inflater.inflate(R.layout.fragment_itunes_search, container, false)
Log.d(TAG, "fragment onCreateView")
gridView = binding.gridView
adapter = ItunesAdapter(requireContext(), ArrayList())
adapter = OnlineFeedsAdapter(requireContext(), ArrayList())
gridView.setAdapter(adapter)
//Show information about the podcast when the list item is clicked
@ -121,24 +115,26 @@ class OnlineSearchFragment
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
val searchItem: MenuItem = toolbar.menu.findItem(R.id.action_search)
val sv = searchItem.actionView as SearchView?
sv!!.queryHint = getString(R.string.search_podcast_hint)
sv.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(s: String): Boolean {
sv.clearFocus()
search(s)
return true
}
val sv = searchItem.actionView as? SearchView
if (sv != null) {
sv.queryHint = getString(R.string.search_podcast_hint)
sv.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(s: String): Boolean {
sv.clearFocus()
search(s)
return true
}
override fun onQueryTextChange(s: String): Boolean {
return false
}
})
sv.setOnQueryTextFocusChangeListener(View.OnFocusChangeListener { view: View, hasFocus: Boolean ->
if (hasFocus) {
showInputMethod(view.findFocus())
}
})
override fun onQueryTextChange(s: String): Boolean {
return false
}
})
sv.setOnQueryTextFocusChangeListener(View.OnFocusChangeListener { view: View, hasFocus: Boolean ->
if (hasFocus) {
showInputMethod(view.findFocus())
}
})
}
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
@ -152,7 +148,7 @@ class OnlineSearchFragment
searchItem.expandActionView()
if (requireArguments().getString(ARG_QUERY, null) != null) {
sv.setQuery(requireArguments().getString(ARG_QUERY, null), true)
sv?.setQuery(requireArguments().getString(ARG_QUERY, null), true)
}
}
@ -167,8 +163,8 @@ class OnlineSearchFragment
adapter?.clear()
if (searchResults != null) adapter?.addAll(searchResults!!)
adapter?.notifyDataSetInvalidated()
gridView.visibility = if (searchResults!!.isNotEmpty()) View.VISIBLE else View.GONE
txtvEmpty.visibility = if (searchResults!!.isEmpty()) View.VISIBLE else View.GONE
gridView.visibility = if (!searchResults.isNullOrEmpty()) View.VISIBLE else View.GONE
txtvEmpty.visibility = if (searchResults.isNullOrEmpty()) View.VISIBLE else View.GONE
txtvEmpty.text = getString(R.string.no_results_for_query) + query
}, { error: Throwable ->
Log.e(TAG, Log.getStackTraceString(error))
@ -194,7 +190,7 @@ class OnlineSearchFragment
}
companion object {
private const val TAG = "FyydSearchFragment"
private const val TAG = "OnlineSearchFragment"
private const val ARG_SEARCHER = "searcher"
private const val ARG_QUERY = "query"

View File

@ -64,6 +64,7 @@ import java.util.*
*/
class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener {
private lateinit var binding: QueueFragmentBinding
private lateinit var infoBar: TextView
private lateinit var recyclerView: EpisodeItemListRecyclerView
private lateinit var emptyView: EmptyViewHandler
@ -88,7 +89,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
val binding = QueueFragmentBinding.inflate(inflater)
binding = QueueFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
toolbar = binding.toolbar
@ -122,6 +123,13 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
swipeActions.setFilter(FeedItemFilter(FeedItemFilter.QUEUED))
swipeActions.attachTo(recyclerView)
if (swipeActions.actions?.left != null) {
binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
recyclerAdapter = object : QueueRecyclerAdapter(activity as MainActivity, swipeActions) {
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
@ -288,6 +296,16 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
refreshToolbarState()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) {
if (swipeActions.actions?.left != null) {
binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
}
if (swipeActions.actions?.right != null) {
binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onKeyUp(event: KeyEvent) {
if (!isAdded || !isVisible || !isMenuVisible) {
@ -456,7 +474,6 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
}
}
info += ""
info += getString(R.string.time_left_label)
info += Converter.getDurationStringLocalized(requireActivity(), timeLeft)
}
infoBar.text = info
@ -503,10 +520,10 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
sortOrder = UserPreferences.queueKeepSortedOrder
}
val view: View = super.onCreateView(inflater, container, savedInstanceState)!!
viewBinding.keepSortedCheckbox.visibility = View.VISIBLE
viewBinding.keepSortedCheckbox.setChecked(UserPreferences.isQueueKeepSorted)
binding.keepSortedCheckbox.visibility = View.VISIBLE
binding.keepSortedCheckbox.setChecked(UserPreferences.isQueueKeepSorted)
// Disable until something gets selected
viewBinding.keepSortedCheckbox.setEnabled(UserPreferences.isQueueKeepSorted)
binding.keepSortedCheckbox.setEnabled(UserPreferences.isQueueKeepSorted)
return view
}
@ -518,11 +535,11 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda
@UnstableApi override fun onSelectionChanged() {
super.onSelectionChanged()
viewBinding.keepSortedCheckbox.setEnabled(sortOrder != SortOrder.RANDOM)
binding.keepSortedCheckbox.setEnabled(sortOrder != SortOrder.RANDOM)
if (sortOrder == SortOrder.RANDOM) {
viewBinding.keepSortedCheckbox.setChecked(false)
binding.keepSortedCheckbox.setChecked(false)
}
UserPreferences.isQueueKeepSorted = viewBinding.keepSortedCheckbox.isChecked
UserPreferences.isQueueKeepSorted = binding.keepSortedCheckbox.isChecked
UserPreferences.queueKeepSortedOrder = sortOrder
DBWriter.reorderQueue(sortOrder, true)
}

View File

@ -19,13 +19,6 @@ class SwipePreferencesFragment : PreferenceFragmentCompat() {
})
true
}
// findPreference<Preference>(PREF_SWIPE_INBOX)?.onPreferenceClickListener =
// Preference.OnPreferenceClickListener {
// SwipeActionsDialog(requireContext(), InboxFragment.TAG).show (object : SwipeActionsDialog.Callback {
// override fun onCall() {}
// })
// true
// }
findPreference<Preference>(PREF_SWIPE_EPISODES)?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
SwipeActionsDialog(requireContext(), AllEpisodesFragment.TAG).show (object : SwipeActionsDialog.Callback {
@ -63,7 +56,6 @@ class SwipePreferencesFragment : PreferenceFragmentCompat() {
companion object {
private const val PREF_SWIPE_QUEUE = "prefSwipeQueue"
// private const val PREF_SWIPE_INBOX = "prefSwipeInbox"
// private const val PREF_SWIPE_STATISTICS = "prefSwipeStatistics"
private const val PREF_SWIPE_EPISODES = "prefSwipeEpisodes"
private const val PREF_SWIPE_DOWNLOADS = "prefSwipeDownloads"

View File

@ -64,11 +64,11 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
showFullNotificationButtonsDialog()
true
}
findPreference<Preference>(UserPreferences.PREF_FILTER_FEED)?.onPreferenceClickListener =
(Preference.OnPreferenceClickListener {
SubscriptionsFilterDialog().show(childFragmentManager, "filter")
true
})
// findPreference<Preference>(UserPreferences.PREF_FILTER_FEED)?.onPreferenceClickListener =
// (Preference.OnPreferenceClickListener {
// SubscriptionsFilterDialog().show(childFragmentManager, "filter")
// true
// })
findPreference<Preference>(UserPreferences.PREF_DRAWER_FEED_ORDER)?.onPreferenceClickListener =
(Preference.OnPreferenceClickListener {

View File

@ -0,0 +1,35 @@
package ac.mdiq.podcini.ui.fragment.swipeactions
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.ui.view.LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary
class NoActionSwipeAction : SwipeAction {
override fun getId(): String {
return SwipeAction.NO_ACTION
}
override fun getActionIcon(): Int {
return R.drawable.ic_questionmark
}
override fun getActionColor(): Int {
return R.attr.icon_red
}
override fun getTitle(context: Context): String {
return context.getString(R.string.no_action_label)
}
@UnstableApi override fun performAction(item: FeedItem, fragment: Fragment, filter: FeedItemFilter) {
}
override fun willRemove(filter: FeedItemFilter, item: FeedItem): Boolean {
return false
}
}

View File

@ -24,6 +24,8 @@ interface SwipeAction {
fun willRemove(filter: FeedItemFilter, item: FeedItem): Boolean
companion object {
const val NO_ACTION: String = "NO_ACTION"
const val ADD_TO_QUEUE: String = "ADD_TO_QUEUE"
const val START_DOWNLOAD: String = "START_DOWNLOAD"
const val MARK_FAV: String = "MARK_FAV"

View File

@ -17,7 +17,10 @@ import ac.mdiq.podcini.ui.fragment.*
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr
import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder
import ac.mdiq.podcini.util.event.PlayerStatusEvent
import ac.mdiq.podcini.util.event.SwipeActionsChangedEvent
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import org.greenrobot.eventbus.EventBus
import java.util.*
import kotlin.math.max
import kotlin.math.min
@ -25,6 +28,7 @@ import kotlin.math.sin
open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private val tag: String) :
ItemTouchHelper.SimpleCallback(dragDirs, ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT), LifecycleObserver {
private var filter: FeedItemFilter? = null
var actions: Actions? = null
@ -73,6 +77,7 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
SwipeActionsDialog(fragment.requireContext(), tag).show(object : SwipeActionsDialog.Callback {
override fun onCall() {
this@SwipeActions.reloadPreference()
EventBus.getDefault().post(SwipeActionsChangedEvent())
}
})
return
@ -147,7 +152,6 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
max(0.5, displacementPercentage.toDouble()).toFloat()))
builder.create().decorate()
super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive)
}
@ -202,7 +206,7 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
}
fun hasActions(): Boolean {
return right != null && left != null
return right != null && left != null && right!!.getId() != SwipeAction.NO_ACTION && left!!.getId() != SwipeAction.NO_ACTION
}
}
@ -213,7 +217,7 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
@JvmField
val swipeActions: List<SwipeAction> = Collections.unmodifiableList(
listOf(AddToQueueSwipeAction(), StartDownloadSwipeAction(), MarkFavoriteSwipeAction(),
listOf(NoActionSwipeAction(), AddToQueueSwipeAction(), StartDownloadSwipeAction(), MarkFavoriteSwipeAction(),
TogglePlaybackStateSwipeAction(), RemoveFromQueueSwipeAction(),
DeleteSwipeAction(), RemoveFromHistorySwipeAction())
)
@ -225,19 +229,18 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
return Actions(prefsString)
}
private fun getPrefs(context: Context, tag: String): Actions {
fun getPrefs(context: Context, tag: String): Actions {
return getPrefs(context, tag, "")
}
@JvmStatic
fun getPrefsWithDefaults(context: Context, tag: String): Actions {
val defaultActions = when (tag) {
// InboxFragment.TAG -> SwipeAction.ADD_TO_QUEUE + "," + SwipeAction.REMOVE_FROM_INBOX
QueueFragment.TAG -> SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE
CompletedDownloadsFragment.TAG -> SwipeAction.DELETE + "," + SwipeAction.DELETE
PlaybackHistoryFragment.TAG -> SwipeAction.REMOVE_FROM_HISTORY + "," + SwipeAction.REMOVE_FROM_HISTORY
AllEpisodesFragment.TAG -> SwipeAction.MARK_FAV + "," + SwipeAction.START_DOWNLOAD
else -> SwipeAction.MARK_FAV + "," + SwipeAction.START_DOWNLOAD
QueueFragment.TAG -> SwipeAction.NO_ACTION + "," + SwipeAction.NO_ACTION
CompletedDownloadsFragment.TAG -> SwipeAction.NO_ACTION + "," + SwipeAction.NO_ACTION
PlaybackHistoryFragment.TAG -> SwipeAction.NO_ACTION + "," + SwipeAction.NO_ACTION
AllEpisodesFragment.TAG -> SwipeAction.NO_ACTION + "," + SwipeAction.NO_ACTION
else -> SwipeAction.NO_ACTION + "," + SwipeAction.NO_ACTION
}
return getPrefs(context, tag, defaultActions)
}

View File

@ -118,21 +118,23 @@ object ChapterUtils {
if (playable.getLocalMediaUrl() == null) {
throw IOException("No local url")
}
val source = File(playable.getLocalMediaUrl()?:"")
val source = File(playable.getLocalMediaUrl() ?: "")
if (!source.exists()) {
throw IOException("Local file does not exist")
}
return CountingInputStream(BufferedInputStream(FileInputStream(source)))
} else if (playable.getStreamUrl() != null && playable.getStreamUrl()!!.startsWith(ContentResolver.SCHEME_CONTENT)) {
val uri = Uri.parse(playable.getStreamUrl())
return CountingInputStream(BufferedInputStream(context.contentResolver.openInputStream(uri)))
} else {
val request: Request = Builder().url(playable.getStreamUrl()?:"").build()
val response = getHttpClient().newCall(request).execute()
if (response.body == null) {
throw IOException("Body is null")
val streamurl = playable.getStreamUrl()
if (streamurl != null && streamurl.startsWith(ContentResolver.SCHEME_CONTENT)) {
val uri = Uri.parse(streamurl)
return CountingInputStream(BufferedInputStream(context.contentResolver.openInputStream(uri)))
} else {
if (streamurl.isNullOrEmpty()) throw IOException("stream url is null of empty")
val request: Request = Builder().url(streamurl).build()
val response = getHttpClient().newCall(request).execute()
if (response.body == null) throw IOException("Body is null")
return CountingInputStream(BufferedInputStream(response.body!!.byteStream()))
}
return CountingInputStream(BufferedInputStream(response.body!!.byteStream()))
}
}

View File

@ -0,0 +1,3 @@
package ac.mdiq.podcini.util.event
class SwipeActionsChangedEvent

View File

@ -0,0 +1,9 @@
<vector android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color"
android:pathData="M3.99,12l3.99,-4v3H20v2H7.99v3z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color"
android:pathData="M16.01,11H4v2h12.01v3L20,12l-3.99,-4z"/>
</vector>

View File

@ -1,5 +1,6 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color" android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
<path android:fillColor="?attr/action_icon_color"
android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z"/>
</vector>

View File

@ -20,18 +20,60 @@
app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator" />
<TextView
android:id="@+id/txtvInformation"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingBottom="8dp"
android:text="@string/filtered_label"
android:layout_below="@id/toolbar"
android:layout_marginTop="-12dp"
android:layout_marginLeft="60dp"
android:layout_marginStart="60dp"
android:visibility="gone"
tools:visibility="visible" />
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/left_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_left_alt_24" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<TextView
android:id="@+id/txtvInformation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackground"
android:text="@string/filtered_label"
android:visibility="gone"
tools:visibility="visible" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_right_alt_24" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/right_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -21,7 +21,6 @@
android:id="@+id/fragment_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:windowContentOverlay"
/>
android:foreground="?android:windowContentOverlay"/>
</LinearLayout>

View File

@ -152,18 +152,61 @@
tools:visibility="visible"
tools:text="(!) Last refresh failed" />
<TextView
android:id="@+id/txtvInformation"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp"
android:background="?android:attr/colorBackground"
android:foreground="?android:attr/selectableItemBackground"
android:visibility="gone"
android:gravity="center"
android:textColor="?attr/colorAccent"
tools:visibility="visible"
tools:text="(i) Information" />
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/left_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_left_alt_24" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<TextView
android:id="@+id/txtvInformation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp"
android:background="?android:attr/colorBackground"
android:foreground="?android:attr/selectableItemBackground"
android:visibility="gone"
android:gravity="center"
android:textColor="?attr/colorAccent"
tools:visibility="visible"
tools:text="(i) Information" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_right_alt_24" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/right_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
</LinearLayout>
<TextView
android:id="@+id/txtvUpdatesDisabled"

View File

@ -41,7 +41,7 @@
android:paddingTop="@dimen/list_vertical_padding"
android:stretchMode="columnWidth"
android:verticalSpacing="8dp"
tools:listitem="@layout/itunes_podcast_listitem" />
tools:listitem="@layout/online_podcast_listitem" />
<TextView
android:id="@android:id/empty"

View File

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/itunes_podcast_listitem"
android:paddingTop="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
tools:background="@android:color/darker_gray">
<ImageView
android:id="@+id/imgvCover"
android:layout_width="@dimen/thumbnail_length_itemlist"
android:layout_height="@dimen/thumbnail_length_itemlist"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:cropToPadding="true"
android:scaleType="fitXY"
tools:background="@android:color/holo_green_dark"
tools:src="@tools:sample/avatars" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/imgvCover"
android:layout_toEndOf="@id/imgvCover"
android:layout_centerVertical="true"
android:orientation="vertical"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp">
<TextView
android:id="@+id/txtvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Podcini.TextView.ListItemPrimaryTitle"
tools:background="@android:color/holo_green_dark"
tools:text="Podcast title" />
<TextView
android:id="@+id/txtvAuthor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="middle"
android:maxLines="2"
style="android:style/TextAppearance.Small"
tools:text="author"
tools:background="@android:color/holo_green_dark" />
</LinearLayout>
</RelativeLayout>

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/itemdescription_listitem"
android:id="@+id/online_item_description"
android:orientation="vertical"
android:paddingHorizontal="20dp"
android:paddingVertical="16dp">

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/online_podcast_listitem"
android:paddingTop="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
tools:background="@android:color/darker_gray">
<TextView
android:id="@+id/txtvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:paddingBottom="4dp"
style="@style/Podcini.TextView.ListItemPrimaryTitle"
tools:background="@android:color/holo_green_dark"
tools:text="Podcast title" />
<ImageView
android:id="@+id/imgvCover"
android:layout_width="@dimen/thumbnail_length_itemlist"
android:layout_height="@dimen/thumbnail_length_itemlist"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@id/txtvTitle"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:cropToPadding="true"
android:scaleType="fitXY"
tools:background="@android:color/holo_green_dark"
tools:src="@tools:sample/avatars" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txtvTitle"
android:layout_toRightOf="@id/imgvCover"
android:layout_toEndOf="@id/imgvCover"
android:layout_centerVertical="true"
android:orientation="vertical"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp">
<TextView
android:id="@+id/txtvAuthor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary"
android:ellipsize="middle"
style="android:style/TextAppearance.Small"
tools:text="author"
tools:background="@android:color/holo_green_dark"
android:singleLine="true"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="1"
style="android:style/TextAppearance.Small"
tools:text="10 episodes"
tools:background="@android:color/holo_green_dark" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<TextView
android:id="@+id/update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="1"
style="android:style/TextAppearance.Small"
tools:text="2023-03-04"
tools:background="@android:color/holo_green_dark" />
</LinearLayout>
<TextView
android:id="@+id/source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textColor="?android:attr/textColorSecondary"
android:ellipsize="middle"
android:maxLines="2"
style="android:style/TextAppearance.Small"
tools:text="directory: http://address"
tools:background="@android:color/holo_green_dark"/>
</LinearLayout>
</RelativeLayout>

View File

@ -161,7 +161,6 @@
android:text="@string/stop_preview"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
<ListView

View File

@ -22,17 +22,58 @@
app:navigationIcon="?homeAsUpIndicator"
app:title="@string/queue_label" />
<TextView
android:id="@+id/info_bar"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:textSize="12sp"
android:layout_marginTop="-12dp"
android:layout_marginLeft="60dp"
android:layout_marginStart="60dp"
android:layout_marginBottom="8dp"
tools:text="12 Episodes - Time remaining: 12 hours" />
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/left_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_left_alt_24" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<TextView
android:id="@+id/info_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:layout_gravity="center_vertical"
tools:text="12 Episodes - 12 hours" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_right_alt_24" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/right_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -19,17 +19,58 @@
app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator" />
<TextView
android:id="@+id/info_bar"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:textSize="12sp"
android:layout_marginTop="-12dp"
android:layout_marginLeft="60dp"
android:layout_marginStart="60dp"
android:layout_marginBottom="8dp"
tools:text="12 Episodes - Size: 40 MB" />
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/left_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_left_alt_24" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<TextView
android:id="@+id/info_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:layout_gravity="center_vertical"
tools:text="12 Episodes - 40 MB" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="fitXY"
app:srcCompat="@drawable/baseline_arrow_right_alt_24" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/right_action_icon"
android:scaleType="fitXY"
app:srcCompat="@drawable/ic_questionmark" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -45,7 +45,7 @@
<item
android:id="@+id/open_feed_item"
android:icon="@drawable/ic_feed"
custom:showAsAction="collapseActionView"
custom:showAsAction="always"
android:title="@string/open_podcast"
android:visible="false">
</item>

View File

@ -10,7 +10,7 @@
<dimen name="text_size_small">14sp</dimen>
<dimen name="text_size_navdrawer">16sp</dimen>
<dimen name="text_size_large">20sp</dimen>
<dimen name="thumbnail_length_itemlist">56dp</dimen>
<dimen name="thumbnail_length_itemlist">65dp</dimen>
<dimen name="thumbnail_length_queue_item">56dp</dimen>
<dimen name="thumbnail_length_onlinefeedview">92dp</dimen>
<dimen name="feeditemlist_header_height">132dp</dimen>

View File

@ -217,6 +217,8 @@
<item quantity="other">%d downloaded episodes deleted.</item>
</plurals>
<string name="no_action_label">No action</string>
<string name="removed_inbox_label">Removed from inbox</string>
<string name="mark_read_label">Mark as played</string>
<string name="toggle_played_label">Toggle played state</string>

View File

@ -5,10 +5,6 @@
android:key="prefSwipeQueue"
android:title="@string/queue_label"/>
<!-- <Preference-->
<!-- android:key="prefSwipeInbox"-->
<!-- android:title="@string/inbox_label"/>-->
<Preference
android:key="prefSwipeEpisodes"
android:title="@string/episodes_label"/>
@ -21,12 +17,12 @@
android:key="prefSwipeHistory"
android:title="@string/playback_history_label"/>
<Preference
android:key="prefSwipeStatistics"
android:title="@string/statistics_label"/>
<!-- <Preference-->
<!-- android:key="prefSwipeStatistics"-->
<!-- android:title="@string/statistics_label"/>-->
<Preference
android:key="prefSwipeFeed"
android:title="@string/individual_subscription"/>
<!-- <Preference-->
<!-- android:key="prefSwipeFeed"-->
<!-- android:title="@string/individual_subscription"/>-->
</PreferenceScreen>

View File

@ -45,10 +45,10 @@
android:key="prefDrawerFeedIndicator"
android:summary="@string/pref_nav_drawer_feed_counter_sum"
android:defaultValue="1"/>
<Preference
android:title="@string/pref_filter_feed_title"
android:key="prefSubscriptionsFilter"
android:summary="@string/pref_filter_feed_sum" />
<!-- <Preference-->
<!-- android:title="@string/pref_filter_feed_title"-->
<!-- android:key="prefSubscriptionsFilter"-->
<!-- android:summary="@string/pref_filter_feed_sum" />-->
<!-- <SwitchPreferenceCompat-->
<!-- android:title="@string/pref_show_subscription_title"-->
<!-- android:key="prefSubscriptionTitle"-->

View File

@ -93,4 +93,15 @@
* renewed PodcastIndex API keys
* added share notes menu option in episode view
* press on title area of an episode now opens the episode info faster and more economically - without horizontal swipe
* press on the icon of an episode opens the episode info the original way - with horizontal swipe
* press on the icon of an episode opens the episode info the original way - with horizontal swipe
## 4.3.0
* added more info about feeds in the online search view
* fixed bug of preview not playing
* disabled feed filters setting in preference
* "open feed" is an action item on audio player top bar
* added swipe action telltales in all episode lists
* added NO_ACTION swipe action
* all default swipe actions are set to NO_ACTION
* cleaned up swipe preferences ui: statistics and individual subs removed

View File

@ -0,0 +1,11 @@
Version 4.3.0 brings several changes:
* added more info about feeds in the online search view
* fixed bug of preview not playing
* disabled feed filters setting in preference
* "open feed" is an action item on audio player top bar
* added swipe action telltales in all episode lists
* added NO_ACTION swipe action
* all default swipe actions are set to NO_ACTION
* cleaned up swipe preferences ui: statistics and individual subs removed

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

View File

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

View File

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

View File

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

BIN
images/2_setting1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
images/3_subscriptions.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
images/4_queue.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

BIN
images/5_podcast.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

BIN
images/5_podcast1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

BIN
images/6_episode.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

BIN
images/7_speed.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
images/8_player.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
images/91_feed_search.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

BIN
images/9_swipe_setting.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
images/9_swipe_setting1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB