6.5.4 commit
This commit is contained in:
parent
943ead25bf
commit
782c582db6
|
@ -31,8 +31,8 @@ android {
|
|||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020237
|
||||
versionName "6.5.3"
|
||||
versionCode 3020238
|
||||
versionName "6.5.4"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -263,7 +263,7 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.OnlineFeedViewActivity"
|
||||
android:name=".ui.activity.ShareReceiverActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:theme="@style/Theme.Podcini.Dark.Translucent"
|
||||
android:label="@string/add_feed_label"
|
||||
|
|
|
@ -218,10 +218,10 @@ class FeedHandler {
|
|||
state.namespaces[uri] = Itunes()
|
||||
Logd(TAG, "Recognized ITunes namespace")
|
||||
}
|
||||
uri == YouTube.NSURI && prefix == YouTube.NSTAG -> {
|
||||
state.namespaces[uri] = YouTube()
|
||||
Logd(TAG, "Recognized YouTube namespace")
|
||||
}
|
||||
// uri == YouTube.NSURI && prefix == YouTube.NSTAG -> {
|
||||
// state.namespaces[uri] = YouTube()
|
||||
// Logd(TAG, "Recognized YouTube namespace")
|
||||
// }
|
||||
uri == SimpleChapters.NSURI && prefix.matches(SimpleChapters.NSTAG.toRegex()) -> {
|
||||
state.namespaces[uri] = SimpleChapters()
|
||||
Logd(TAG, "Recognized SimpleChapters namespace")
|
||||
|
@ -238,6 +238,7 @@ class FeedHandler {
|
|||
state.namespaces[uri] = PodcastIndex()
|
||||
Logd(TAG, "Recognized PodcastIndex namespace")
|
||||
}
|
||||
else -> Logd(TAG, "startPrefixMapping can not handle uri: $uri")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import ac.mdiq.podcini.net.feed.parser.element.SyndElement
|
|||
import ac.mdiq.podcini.net.feed.parser.utils.DurationParser.inMillis
|
||||
import org.xml.sax.Attributes
|
||||
|
||||
// TODO: this appears not needed
|
||||
class YouTube : Namespace() {
|
||||
val TAG = this::class.simpleName ?: "Anonymous"
|
||||
|
||||
|
|
|
@ -138,9 +138,7 @@ abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
|
|||
try {
|
||||
activity.unregisterReceiver(statusUpdate)
|
||||
activity.unregisterReceiver(notificationReceiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignore
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {/* ignore */ }
|
||||
initialized = false
|
||||
cancelFlowEvents()
|
||||
}
|
||||
|
|
|
@ -181,6 +181,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||
private fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) {
|
||||
Logd(TAG, "setDataSource1 called")
|
||||
val url = media.getStreamUrl() ?: return
|
||||
val preferences = media.episodeOrFetch()?.feed?.preferences
|
||||
val user = preferences?.username
|
||||
|
@ -191,7 +192,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
val vService = Vista.getService(0)
|
||||
val streamInfo = StreamInfo.getInfo(vService, url)
|
||||
val audioStreamsList = getFilteredAudioStreams(streamInfo.audioStreams)
|
||||
Logd(TAG, "setDataSource1 got ${audioStreamsList.size}")
|
||||
Logd(TAG, "setDataSource1 audioStreamsList ${audioStreamsList.size}")
|
||||
val audioIndex = if (isNetworkRestricted) 0 else audioStreamsList.size - 1
|
||||
val audioStream = audioStreamsList[audioIndex]
|
||||
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
|
||||
|
@ -346,31 +347,37 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
callback.ensureMediaInfoLoaded(curMedia!!)
|
||||
callback.onMediaChanged(false)
|
||||
setPlaybackParams(getCurrentPlaybackSpeed(curMedia), UserPreferences.isSkipSilence)
|
||||
when {
|
||||
streaming -> {
|
||||
val streamurl = curMedia!!.getStreamUrl()
|
||||
if (streamurl != null) {
|
||||
val media = curMedia
|
||||
if (media is EpisodeMedia) {
|
||||
val deferred = CoroutineScope(Dispatchers.IO).async { setDataSource(metadata, media) }
|
||||
if (startWhenPrepared) runBlocking { deferred.await() }
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
when {
|
||||
streaming -> {
|
||||
val streamurl = curMedia!!.getStreamUrl()
|
||||
if (streamurl != null) {
|
||||
val media = curMedia
|
||||
if (media is EpisodeMedia) {
|
||||
mediaItem = null
|
||||
mediaSource = null
|
||||
setDataSource(metadata, media)
|
||||
// val deferred = CoroutineScope(Dispatchers.IO).async { setDataSource(metadata, media) }
|
||||
// if (startWhenPrepared) runBlocking { deferred.await() }
|
||||
// val preferences = media.episodeOrFetch()?.feed?.preferences
|
||||
// setDataSource(metadata, streamurl, preferences?.username, preferences?.password)
|
||||
} else setDataSource(metadata, streamurl, null, null)
|
||||
} else setDataSource(metadata, streamurl, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val localMediaurl = curMedia!!.getLocalMediaUrl()
|
||||
else -> {
|
||||
val localMediaurl = curMedia!!.getLocalMediaUrl()
|
||||
// File(localMediaurl).canRead() time consuming, leave it to MediaItem to handle
|
||||
// if (!localMediaurl.isNullOrEmpty() && File(localMediaurl).canRead()) setDataSource(metadata, localMediaurl, null, null)
|
||||
if (!localMediaurl.isNullOrEmpty()) setDataSource(metadata, localMediaurl, null, null)
|
||||
else throw IOException("Unable to read local file $localMediaurl")
|
||||
if (!localMediaurl.isNullOrEmpty()) setDataSource(metadata, localMediaurl, null, null)
|
||||
else throw IOException("Unable to read local file $localMediaurl")
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, curMedia)
|
||||
if (prepareImmediately) prepare()
|
||||
}
|
||||
}
|
||||
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, curMedia)
|
||||
|
||||
if (prepareImmediately) prepare()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
setPlayerStatus(PlayerStatus.ERROR, null)
|
||||
|
@ -394,6 +401,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
seekTo(newPosition)
|
||||
}
|
||||
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR()
|
||||
// while (mediaItem == null && mediaSource == null) runBlocking { delay(100) }
|
||||
exoPlayer?.play()
|
||||
// Can't set params when paused - so always set it on start in case they changed
|
||||
exoPlayer?.playbackParameters = playbackParameters
|
||||
|
|
|
@ -101,7 +101,6 @@ import kotlin.math.max
|
|||
*/
|
||||
@UnstableApi
|
||||
class PlaybackService : MediaLibraryService() {
|
||||
|
||||
private var mediaSession: MediaLibrarySession? = null
|
||||
|
||||
internal var mPlayer: MediaPlayerBase? = null
|
||||
|
@ -230,6 +229,37 @@ class PlaybackService : MediaLibraryService() {
|
|||
}
|
||||
}
|
||||
|
||||
private val shutdownReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.d(TAG, "shutdownReceiver onReceive called with action: ${intent.action}")
|
||||
if (intent.action == ACTION_SHUTDOWN_PLAYBACK_SERVICE)
|
||||
EventFlow.postEvent(FlowEvent.PlaybackServiceEvent(FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN))
|
||||
}
|
||||
}
|
||||
|
||||
val rootItem = MediaItem.Builder()
|
||||
.setMediaId("CurQueue")
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setIsBrowsable(true)
|
||||
.setIsPlayable(false)
|
||||
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
|
||||
.setTitle(curQueue.name)
|
||||
.build())
|
||||
.build()
|
||||
|
||||
val mediaItemsInQueue: MutableList<MediaItem> by lazy {
|
||||
val list = mutableListOf<MediaItem>()
|
||||
curQueue.episodes.forEach {
|
||||
if (it.media != null) {
|
||||
val item = buildMediaItem(it.media!!)
|
||||
if (item != null) list += item
|
||||
}
|
||||
}
|
||||
Logd(TAG, "mediaItemsInQueue: ${list.size}")
|
||||
list
|
||||
}
|
||||
|
||||
private val mediaPlayerCallback: MediaPlayerCallback = object : MediaPlayerCallback {
|
||||
override fun statusChanged(newInfo: MediaPlayerInfo?) {
|
||||
currentMediaType = mPlayer?.mediaType ?: MediaType.UNKNOWN
|
||||
|
@ -391,7 +421,6 @@ class PlaybackService : MediaLibraryService() {
|
|||
j = if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) curIndexInQueue else eList.size-1
|
||||
} else if (i < eList.size-1) j = i+1
|
||||
Logd(TAG, "getNextInQueue next j: $j")
|
||||
|
||||
val nextItem = unmanaged(eList[j])
|
||||
Logd(TAG, "getNextInQueue nextItem ${nextItem.title}")
|
||||
if (nextItem.media == null) {
|
||||
|
@ -399,13 +428,11 @@ class PlaybackService : MediaLibraryService() {
|
|||
writeNoMediaPlaying()
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isFollowQueue) {
|
||||
Logd(TAG, "getNextInQueue(), but follow queue is not enabled.")
|
||||
writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed?.isLocalFeed != true) {
|
||||
Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}")
|
||||
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent)
|
||||
|
@ -433,11 +460,9 @@ class PlaybackService : MediaLibraryService() {
|
|||
else -> EXTRA_CODE_AUDIO
|
||||
})
|
||||
}
|
||||
|
||||
override fun ensureMediaInfoLoaded(media: Playable) {
|
||||
// if (media is EpisodeMedia && media.item == null) media.item = DBReader.getFeedItem(media.itemId)
|
||||
}
|
||||
|
||||
fun writeMediaPlaying(playable: Playable?, playerStatus: PlayerStatus) {
|
||||
Logd(InTheatre.TAG, "Writing playback preferences ${playable?.getIdentifier()}")
|
||||
if (playable == null) writeNoMediaPlaying()
|
||||
|
@ -457,14 +482,12 @@ class PlaybackService : MediaLibraryService() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun writePlayerStatus(playerStatus: PlayerStatus) {
|
||||
Logd(InTheatre.TAG, "Writing player status playback preferences")
|
||||
curState = upsertBlk(curState) {
|
||||
it.curPlayerStatus = getCurPlayerStatusAsInt(playerStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurPlayerStatusAsInt(playerStatus: PlayerStatus): Int {
|
||||
val playerStatusAsInt = when (playerStatus) {
|
||||
PlayerStatus.PLAYING -> PLAYER_STATUS_PLAYING
|
||||
|
@ -475,37 +498,6 @@ class PlaybackService : MediaLibraryService() {
|
|||
}
|
||||
}
|
||||
|
||||
private val shutdownReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.d(TAG, "shutdownReceiver onReceive called with action: ${intent.action}")
|
||||
if (intent.action == ACTION_SHUTDOWN_PLAYBACK_SERVICE)
|
||||
EventFlow.postEvent(FlowEvent.PlaybackServiceEvent(FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN))
|
||||
}
|
||||
}
|
||||
|
||||
val rootItem = MediaItem.Builder()
|
||||
.setMediaId("CurQueue")
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setIsBrowsable(true)
|
||||
.setIsPlayable(false)
|
||||
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
|
||||
.setTitle(curQueue.name)
|
||||
.build())
|
||||
.build()
|
||||
|
||||
val mediaItemsInQueue: MutableList<MediaItem> by lazy {
|
||||
val list = mutableListOf<MediaItem>()
|
||||
curQueue.episodes.forEach {
|
||||
if (it.media != null) {
|
||||
val item = buildMediaItem(it.media!!)
|
||||
if (item != null) list += item
|
||||
}
|
||||
}
|
||||
Logd(TAG, "mediaItemsInQueue: ${list.size}")
|
||||
list
|
||||
}
|
||||
|
||||
private val mediaLibrarySessionCK = object: MediaLibrarySession.Callback {
|
||||
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
||||
Logd(TAG, "in MyMediaSessionCallback onConnect")
|
||||
|
@ -762,9 +754,9 @@ class PlaybackService : MediaLibraryService() {
|
|||
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
|
||||
val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
|
||||
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
|
||||
val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||
val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU)
|
||||
intent?.getParcelableExtra(EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||
} else {
|
||||
else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent?.getParcelableExtra(EXTRA_KEY_EVENT)
|
||||
}
|
||||
|
@ -797,6 +789,7 @@ class PlaybackService : MediaLibraryService() {
|
|||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
playable != null -> {
|
||||
recreateMediaSessionIfNeeded()
|
||||
Logd(TAG, "onStartCommand status: $status")
|
||||
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
|
||||
val allowStreamAlways = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_ALWAYS, false) ?: false
|
||||
|
@ -963,6 +956,7 @@ class PlaybackService : MediaLibraryService() {
|
|||
}
|
||||
|
||||
private fun startPlayingFromPreferences() {
|
||||
recreateMediaSessionIfNeeded()
|
||||
scope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { loadPlayableFromPreferences() }
|
||||
|
@ -975,7 +969,7 @@ class PlaybackService : MediaLibraryService() {
|
|||
}
|
||||
|
||||
private fun startPlaying(allowStreamThisTime: Boolean) {
|
||||
Logd(TAG, "startPlaying called $allowStreamThisTime")
|
||||
Logd(TAG, "startPlaying called allowStreamThisTime: $allowStreamThisTime")
|
||||
val media = curMedia ?: return
|
||||
|
||||
val localFeed = URLUtil.isContentUrl(media.getStreamUrl())
|
||||
|
@ -986,18 +980,17 @@ class PlaybackService : MediaLibraryService() {
|
|||
return
|
||||
}
|
||||
|
||||
if (media.getIdentifier() != curState.curMediaId) clearCurTempSpeed()
|
||||
// TODO: this is redundant
|
||||
// if (media.getIdentifier() != curState.curMediaId) clearCurTempSpeed()
|
||||
|
||||
mPlayer?.playMediaObject(media, streaming, startWhenPrepared = true, true)
|
||||
recreateMediaSessionIfNeeded()
|
||||
// recreateMediaSessionIfNeeded()
|
||||
// val episode = (media as? EpisodeMedia)?.episode
|
||||
// if (curMedia is EpisodeMedia && episode != null) addToQueue(true, episode)
|
||||
}
|
||||
|
||||
fun clearCurTempSpeed() {
|
||||
curState = upsertBlk(curState) {
|
||||
it.curTempSpeed = FeedPreferences.SPEED_USE_GLOBAL
|
||||
}
|
||||
curState = upsertBlk(curState) { it.curTempSpeed = FeedPreferences.SPEED_USE_GLOBAL }
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions.menuhandler
|
||||
package ac.mdiq.podcini.ui.actions.handler
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions
|
||||
package ac.mdiq.podcini.ui.actions.handler
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.ui.actions.menuhandler
|
||||
package ac.mdiq.podcini.ui.actions.handler
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
|
@ -1,38 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Queues.addToQueue
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class AddToQueueSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.ADD_TO_QUEUE
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_playlist_play
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return androidx.appcompat.R.attr.colorAccent
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.add_to_queue_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
addToQueue( true, item)
|
||||
// else RemoveFromQueueSwipeAction().performAction(item, fragment, filter)
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showQueued || filter.showNew
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class DeleteSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.DELETE
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_delete
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_red
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.delete_episode_label)
|
||||
}
|
||||
|
||||
@UnstableApi override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
if (!item.isDownloaded && item.feed?.isLocalFeed != true) return
|
||||
deleteEpisodesWarnLocal(fragment.requireContext(), listOf(item))
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showDownloaded && (item.isDownloaded || item.feed?.isLocalFeed == true)
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class MarkFavoriteSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.MARK_FAV
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_star
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_yellow
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.add_to_favorite_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
setFavorite(item, !item.isFavorite)
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showIsFavorite || filter.showNotFavorite
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.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.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
|
||||
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: Episode, fragment: Fragment, filter: EpisodeFilter) {}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.util.*
|
||||
|
||||
class RemoveFromHistorySwipeAction : SwipeAction {
|
||||
val TAG = this::class.simpleName ?: "Anonymous"
|
||||
|
||||
override fun getId(): String {
|
||||
return SwipeAction.REMOVE_FROM_HISTORY
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_history_remove
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_purple
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.remove_history_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val playbackCompletionDate: Date? = item.media?.playbackCompletionDate
|
||||
deleteFromHistory(item)
|
||||
|
||||
(fragment.requireActivity() as MainActivity)
|
||||
.showSnackbarAbovePlayer(R.string.removed_history_label, Snackbar.LENGTH_LONG)
|
||||
.setAction(fragment.getString(R.string.undo)) {
|
||||
if (playbackCompletionDate != null) addToHistory(item, playbackCompletionDate) }
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun deleteFromHistory(episode: Episode) {
|
||||
addToHistory(episode, Date(0))
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class RemoveFromQueueSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.REMOVE_FROM_QUEUE
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_playlist_remove
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return androidx.appcompat.R.attr.colorAccent
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.remove_from_queue_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val position: Int = curQueue.episodes.indexOf(item)
|
||||
removeFromQueue(item)
|
||||
if (willRemove(filter, item)) {
|
||||
(fragment.requireActivity() as MainActivity).showSnackbarAbovePlayer(fragment.resources.getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), Snackbar.LENGTH_LONG)
|
||||
.setAction(fragment.getString(R.string.undo)) {
|
||||
addToQueueAt(item, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showQueued || filter.showNotQueued
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a Episode in the queue at the specified index. The 'read'-attribute of the Episode will be set to
|
||||
* true. If the Episode is already in the queue, the queue will not be modified.
|
||||
* @param episode the Episode that should be added to the queue.
|
||||
* @param index Destination index. Must be in range 0..queue.size()
|
||||
* @throws IndexOutOfBoundsException if index < 0 || index >= queue.size()
|
||||
*/
|
||||
@UnstableApi
|
||||
fun addToQueueAt(episode: Episode, index: Int) : Job {
|
||||
return runOnIOScope {
|
||||
if (curQueue.episodeIds.contains(episode.id)) return@runOnIOScope
|
||||
if (episode.isNew) setPlayState(Episode.PlayState.UNPLAYED.code, false, episode)
|
||||
curQueue = upsert(curQueue) {
|
||||
it.episodeIds.add(index, episode.id)
|
||||
it.update()
|
||||
}
|
||||
// curQueue.episodes.add(index, episode)
|
||||
EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, index))
|
||||
// if (performAutoDownload) autodownloadEpisodeMedia(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
|
||||
class ShowFirstSwipeDialogAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return "SHOW_FIRST_SWIPE_DIALOG"
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_settings
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
//handled in SwipeActions
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
|
||||
class StartDownloadSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.START_DOWNLOAD
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_download
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_green
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.download_label)
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
if (!item.isDownloaded && item.feed != null && !item.feed!!.isLocalFeed) {
|
||||
DownloadActionButton(item).onClick(fragment.requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,20 +1,41 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
|
||||
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
|
||||
import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue
|
||||
import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem
|
||||
import ac.mdiq.podcini.storage.database.Queues.addToQueue
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction.Companion.NO_ACTION
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog
|
||||
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
|
||||
import ac.mdiq.podcini.ui.fragment.DownloadsFragment
|
||||
import ac.mdiq.podcini.ui.fragment.HistoryFragment
|
||||
import ac.mdiq.podcini.ui.fragment.QueuesFragment
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.ui.view.EpisodeViewHolder
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Canvas
|
||||
import android.os.Handler
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.fragment.app.Fragment
|
||||
|
@ -23,7 +44,13 @@ import androidx.media3.common.util.UnstableApi
|
|||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.annimon.stream.Stream
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sin
|
||||
|
@ -245,4 +272,327 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
|||
return prefs!!.getBoolean(KEY_PREFIX_NO_ACTION + tag, true)
|
||||
}
|
||||
}
|
||||
|
||||
class AddToQueueSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.ADD_TO_QUEUE
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_playlist_play
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return androidx.appcompat.R.attr.colorAccent
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.add_to_queue_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
addToQueue( true, item)
|
||||
// else RemoveFromQueueSwipeAction().performAction(item, fragment, filter)
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showQueued || filter.showNew
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.DELETE
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_delete
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_red
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.delete_episode_label)
|
||||
}
|
||||
|
||||
@UnstableApi override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
if (!item.isDownloaded && item.feed?.isLocalFeed != true) return
|
||||
deleteEpisodesWarnLocal(fragment.requireContext(), listOf(item))
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showDownloaded && (item.isDownloaded || item.feed?.isLocalFeed == true)
|
||||
}
|
||||
}
|
||||
|
||||
class MarkFavoriteSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.MARK_FAV
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_star
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_yellow
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.add_to_favorite_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
setFavorite(item, !item.isFavorite)
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showIsFavorite || filter.showNotFavorite
|
||||
}
|
||||
}
|
||||
|
||||
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: Episode, fragment: Fragment, filter: EpisodeFilter) {}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveFromHistorySwipeAction : SwipeAction {
|
||||
val TAG = this::class.simpleName ?: "Anonymous"
|
||||
|
||||
override fun getId(): String {
|
||||
return SwipeAction.REMOVE_FROM_HISTORY
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_history_remove
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_purple
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.remove_history_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val playbackCompletionDate: Date? = item.media?.playbackCompletionDate
|
||||
deleteFromHistory(item)
|
||||
|
||||
(fragment.requireActivity() as MainActivity)
|
||||
.showSnackbarAbovePlayer(R.string.removed_history_label, Snackbar.LENGTH_LONG)
|
||||
.setAction(fragment.getString(R.string.undo)) {
|
||||
if (playbackCompletionDate != null) addToHistory(item, playbackCompletionDate) }
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun deleteFromHistory(episode: Episode) {
|
||||
addToHistory(episode, Date(0))
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveFromQueueSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.REMOVE_FROM_QUEUE
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_playlist_remove
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return androidx.appcompat.R.attr.colorAccent
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.remove_from_queue_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val position: Int = curQueue.episodes.indexOf(item)
|
||||
removeFromQueue(item)
|
||||
if (willRemove(filter, item)) {
|
||||
(fragment.requireActivity() as MainActivity).showSnackbarAbovePlayer(fragment.resources.getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1), Snackbar.LENGTH_LONG)
|
||||
.setAction(fragment.getString(R.string.undo)) {
|
||||
addToQueueAt(item, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return filter.showQueued || filter.showNotQueued
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a Episode in the queue at the specified index. The 'read'-attribute of the Episode will be set to
|
||||
* true. If the Episode is already in the queue, the queue will not be modified.
|
||||
* @param episode the Episode that should be added to the queue.
|
||||
* @param index Destination index. Must be in range 0..queue.size()
|
||||
* @throws IndexOutOfBoundsException if index < 0 || index >= queue.size()
|
||||
*/
|
||||
@UnstableApi
|
||||
fun addToQueueAt(episode: Episode, index: Int) : Job {
|
||||
return runOnIOScope {
|
||||
if (curQueue.episodeIds.contains(episode.id)) return@runOnIOScope
|
||||
if (episode.isNew) setPlayState(Episode.PlayState.UNPLAYED.code, false, episode)
|
||||
curQueue = upsert(curQueue) {
|
||||
it.episodeIds.add(index, episode.id)
|
||||
it.update()
|
||||
}
|
||||
// curQueue.episodes.add(index, episode)
|
||||
EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, index))
|
||||
// if (performAutoDownload) autodownloadEpisodeMedia(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShowFirstSwipeDialogAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return "SHOW_FIRST_SWIPE_DIALOG"
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_settings
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
//handled in SwipeActions
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class StartDownloadSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.START_DOWNLOAD
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_download
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_green
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.download_label)
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
if (!item.isDownloaded && item.feed != null && !item.feed!!.isLocalFeed) {
|
||||
DownloadActionButton(item).onClick(fragment.requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class TogglePlaybackStateSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.TOGGLE_PLAYED
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_mark_played
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.toggle_played_label)
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val newState = if (item.playState == Episode.PlayState.UNPLAYED.code) Episode.PlayState.PLAYED.code else Episode.PlayState.UNPLAYED.code
|
||||
|
||||
Logd("TogglePlaybackStateSwipeAction", "performAction( ${item.id} )")
|
||||
// we're marking it as unplayed since the user didn't actually play it
|
||||
// but they don't want it considered 'NEW' anymore
|
||||
var item = runBlocking { setPlayStateSync(newState, false, item) }
|
||||
|
||||
val h = Handler(fragment.requireContext().mainLooper)
|
||||
val r = Runnable {
|
||||
val media: EpisodeMedia? = item.media
|
||||
val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
|
||||
if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
|
||||
item = deleteMediaSync(fragment.requireContext(), item)
|
||||
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) }
|
||||
}
|
||||
val playStateStringRes: Int = when (newState) {
|
||||
Episode.PlayState.UNPLAYED.code -> if (item.playState == Episode.PlayState.NEW.code) R.string.removed_inbox_label //was new
|
||||
else R.string.marked_as_unplayed_label //was played
|
||||
Episode.PlayState.PLAYED.code -> R.string.marked_as_played_label
|
||||
else -> if (item.playState == Episode.PlayState.NEW.code) R.string.removed_inbox_label
|
||||
else R.string.marked_as_unplayed_label
|
||||
}
|
||||
val duration: Int = Snackbar.LENGTH_LONG
|
||||
|
||||
if (willRemove(filter, item)) {
|
||||
(fragment.activity as MainActivity).showSnackbarAbovePlayer(
|
||||
playStateStringRes, duration)
|
||||
.setAction(fragment.getString(R.string.undo)) {
|
||||
setPlayState(item.playState, false, item)
|
||||
// don't forget to cancel the thing that's going to remove the media
|
||||
h.removeCallbacks(r)
|
||||
}
|
||||
}
|
||||
|
||||
h.postDelayed(r, ceil((duration * 1.05f).toDouble()).toLong())
|
||||
}
|
||||
|
||||
private fun delayedExecution(item: Episode, fragment: Fragment, duration: Float) = runBlocking {
|
||||
delay(ceil((duration * 1.05f).toDouble()).toLong())
|
||||
val media: EpisodeMedia? = item.media
|
||||
val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
|
||||
if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
|
||||
// deleteMediaOfEpisode(fragment.requireContext(), item)
|
||||
var item = deleteMediaSync(fragment.requireContext(), item)
|
||||
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) }
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return if (item.playState == Episode.PlayState.NEW.code) filter.showPlayed || filter.showNew
|
||||
else filter.showUnplayed || filter.showPlayed || filter.showNew
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
|
||||
import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue
|
||||
import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.os.Handler
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.math.ceil
|
||||
|
||||
class TogglePlaybackStateSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.TOGGLE_PLAYED
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
return R.drawable.ic_mark_played
|
||||
}
|
||||
|
||||
override fun getActionColor(): Int {
|
||||
return R.attr.icon_gray
|
||||
}
|
||||
|
||||
override fun getTitle(context: Context): String {
|
||||
return context.getString(R.string.toggle_played_label)
|
||||
}
|
||||
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val newState = if (item.playState == Episode.PlayState.UNPLAYED.code) Episode.PlayState.PLAYED.code else Episode.PlayState.UNPLAYED.code
|
||||
|
||||
Logd("TogglePlaybackStateSwipeAction", "performAction( ${item.id} )")
|
||||
// we're marking it as unplayed since the user didn't actually play it
|
||||
// but they don't want it considered 'NEW' anymore
|
||||
var item = runBlocking { setPlayStateSync(newState, false, item) }
|
||||
|
||||
val h = Handler(fragment.requireContext().mainLooper)
|
||||
val r = Runnable {
|
||||
val media: EpisodeMedia? = item.media
|
||||
val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
|
||||
if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
|
||||
item = deleteMediaSync(fragment.requireContext(), item)
|
||||
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) }
|
||||
}
|
||||
val playStateStringRes: Int = when (newState) {
|
||||
Episode.PlayState.UNPLAYED.code -> if (item.playState == Episode.PlayState.NEW.code) R.string.removed_inbox_label //was new
|
||||
else R.string.marked_as_unplayed_label //was played
|
||||
Episode.PlayState.PLAYED.code -> R.string.marked_as_played_label
|
||||
else -> if (item.playState == Episode.PlayState.NEW.code) R.string.removed_inbox_label
|
||||
else R.string.marked_as_unplayed_label
|
||||
}
|
||||
val duration: Int = Snackbar.LENGTH_LONG
|
||||
|
||||
if (willRemove(filter, item)) {
|
||||
(fragment.activity as MainActivity).showSnackbarAbovePlayer(
|
||||
playStateStringRes, duration)
|
||||
.setAction(fragment.getString(R.string.undo)) {
|
||||
setPlayState(item.playState, false, item)
|
||||
// don't forget to cancel the thing that's going to remove the media
|
||||
h.removeCallbacks(r)
|
||||
}
|
||||
}
|
||||
|
||||
h.postDelayed(r, ceil((duration * 1.05f).toDouble()).toLong())
|
||||
}
|
||||
|
||||
private fun delayedExecution(item: Episode, fragment: Fragment, duration: Float) = runBlocking {
|
||||
delay(ceil((duration * 1.05f).toDouble()).toLong())
|
||||
val media: EpisodeMedia? = item.media
|
||||
val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
|
||||
if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
|
||||
// deleteMediaOfEpisode(fragment.requireContext(), item)
|
||||
var item = deleteMediaSync(fragment.requireContext(), item)
|
||||
if (shouldDeleteRemoveFromQueue()) removeFromQueueSync(null, item) }
|
||||
}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return if (item.playState == Episode.PlayState.NEW.code) filter.showPlayed || filter.showNew
|
||||
else filter.showUnplayed || filter.showPlayed || filter.showNew
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
|||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
|
||||
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
|
@ -417,7 +418,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment()
|
||||
DownloadsFragment.TAG -> fragment = DownloadsFragment()
|
||||
HistoryFragment.TAG -> fragment = HistoryFragment()
|
||||
AddFeedFragment.TAG -> fragment = AddFeedFragment()
|
||||
OnlineSearchFragment.TAG -> fragment = OnlineSearchFragment()
|
||||
SubscriptionsFragment.TAG -> fragment = SubscriptionsFragment()
|
||||
StatisticsFragment.TAG -> fragment = StatisticsFragment()
|
||||
else -> {
|
||||
|
@ -622,11 +623,11 @@ class MainActivity : CastEnabledActivity() {
|
|||
when {
|
||||
intent.hasExtra(Extras.fragment_feed_id.name) -> {
|
||||
val feedId = intent.getLongExtra(Extras.fragment_feed_id.name, 0)
|
||||
val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS)
|
||||
val args = intent.getBundleExtra(MainActivityStarter.Extras.fragment_args.name)
|
||||
if (feedId > 0) {
|
||||
val startedFromSearch = intent.getBooleanExtra(Extras.started_from_search.name, false)
|
||||
val startedFromShare = intent.getBooleanExtra(Extras.started_from_share.name, false)
|
||||
val addToBackStack = intent.getBooleanExtra(Extras.add_to_back_stack.name, false)
|
||||
if (startedFromSearch || addToBackStack) loadChildFragment(FeedEpisodesFragment.newInstance(feedId))
|
||||
if (startedFromShare || addToBackStack) loadChildFragment(FeedEpisodesFragment.newInstance(feedId))
|
||||
else loadFeedFragmentById(feedId, args)
|
||||
}
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
|
@ -635,25 +636,26 @@ class MainActivity : CastEnabledActivity() {
|
|||
val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name)
|
||||
if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl))
|
||||
}
|
||||
intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG) -> {
|
||||
val tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)
|
||||
val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS)
|
||||
intent.hasExtra(Extras.search_string.name) -> {
|
||||
val query = intent.getStringExtra(Extras.search_string.name)
|
||||
if (query != null) loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
}
|
||||
intent.hasExtra(MainActivityStarter.Extras.fragment_tag.name) -> {
|
||||
val tag = intent.getStringExtra(MainActivityStarter.Extras.fragment_tag.name)
|
||||
val args = intent.getBundleExtra(MainActivityStarter.Extras.fragment_args.name)
|
||||
if (tag != null) loadFragment(tag, args)
|
||||
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false) -> {
|
||||
intent.getBooleanExtra(MainActivityStarter.Extras.open_player.name, false) -> {
|
||||
// bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
// bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
}
|
||||
else -> handleDeeplink(intent.data)
|
||||
}
|
||||
|
||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false)) drawerLayout?.open()
|
||||
|
||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false))
|
||||
if (intent.getBooleanExtra(MainActivityStarter.Extras.open_drawer.name, false)) drawerLayout?.open()
|
||||
if (intent.getBooleanExtra(MainActivityStarter.Extras.open_download_logs.name, false))
|
||||
DownloadLogFragment().show(supportFragmentManager, null)
|
||||
|
||||
if (intent.getBooleanExtra(Extras.refresh_on_start.name, false)) runOnceOrAsk(this)
|
||||
|
||||
// to avoid handling the intent twice when the configuration changes
|
||||
|
@ -756,9 +758,10 @@ class MainActivity : CastEnabledActivity() {
|
|||
fragment_feed_id,
|
||||
fragment_feed_url,
|
||||
refresh_on_start,
|
||||
started_from_search,
|
||||
started_from_share, // TODO: seems not needed
|
||||
add_to_back_stack,
|
||||
generated_view_id,
|
||||
search_string,
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -781,5 +784,13 @@ class MainActivity : CastEnabledActivity() {
|
|||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
return intent
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showOnlineSearch(context: Context, query: String): Intent {
|
||||
val intent = Intent(context.applicationContext, MainActivity::class.java)
|
||||
intent.putExtra(Extras.search_string.name, query)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ import androidx.media3.common.util.UnstableApi
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.net.URLDecoder
|
||||
|
||||
// this now is only used for receiving shared feed url
|
||||
class OnlineFeedViewActivity : AppCompatActivity() {
|
||||
class ShareReceiverActivity : AppCompatActivity() {
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -31,21 +31,29 @@ class OnlineFeedViewActivity : AppCompatActivity() {
|
|||
if (urlString != null) feedUrl = URLDecoder.decode(urlString, "UTF-8")
|
||||
}
|
||||
|
||||
if (feedUrl == null) {
|
||||
Log.e(TAG, "feedUrl is null.")
|
||||
showNoPodcastFoundError()
|
||||
} else {
|
||||
Logd(TAG, "Activity was started with url $feedUrl")
|
||||
val intent = MainActivity.showOnlineFeed(this, feedUrl)
|
||||
intent.putExtra(MainActivity.Extras.started_from_search.name, getIntent().getBooleanExtra(MainActivity.Extras.started_from_search.name, false))
|
||||
startActivity(intent)
|
||||
finish()
|
||||
when {
|
||||
feedUrl.isNullOrBlank() -> {
|
||||
Log.e(TAG, "feedUrl is empty or null.")
|
||||
showNoPodcastFoundError()
|
||||
}
|
||||
!feedUrl.matches(Regex("[./%]")) -> {
|
||||
val intent = MainActivity.showOnlineSearch(this, feedUrl)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
else -> {
|
||||
Logd(TAG, "Activity was started with url $feedUrl")
|
||||
val intent = MainActivity.showOnlineFeed(this, feedUrl)
|
||||
// intent.putExtra(MainActivity.Extras.started_from_share.name, getIntent().getBooleanExtra(MainActivity.Extras.started_from_share.name, false))
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNoPodcastFoundError() {
|
||||
runOnUiThread {
|
||||
MaterialAlertDialogBuilder(this@OnlineFeedViewActivity)
|
||||
MaterialAlertDialogBuilder(this@ShareReceiverActivity)
|
||||
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> finish() }
|
||||
.setTitle(R.string.error_label)
|
||||
.setMessage(R.string.null_value_podcast_error)
|
||||
|
@ -64,6 +72,6 @@ class OnlineFeedViewActivity : AppCompatActivity() {
|
|||
companion object {
|
||||
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||
private const val RESULT_ERROR = 2
|
||||
private val TAG: String = OnlineFeedViewActivity::class.simpleName ?: "Anonymous"
|
||||
private val TAG: String = ShareReceiverActivity::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
}
|
|
@ -283,9 +283,8 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
videoEpisodeFragment.isFavorite = false
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
item.itemId == R.id.disable_sleeptimer_item || item.itemId == R.id.set_sleeptimer_item -> {
|
||||
item.itemId == R.id.disable_sleeptimer_item || item.itemId == R.id.set_sleeptimer_item ->
|
||||
SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
|
||||
}
|
||||
item.itemId == R.id.audio_controls -> {
|
||||
val dialog = PlaybackControlsDialog.newInstance()
|
||||
dialog.show(supportFragmentManager, "playback_controls")
|
||||
|
@ -374,24 +373,22 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
private lateinit var dialog: AlertDialog
|
||||
private var _binding: AudioControlsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var controller: ServiceStatusHandler? = null
|
||||
private var statusHandler: ServiceStatusHandler? = null
|
||||
|
||||
@UnstableApi override fun onStart() {
|
||||
super.onStart()
|
||||
controller = object : ServiceStatusHandler(requireActivity()) {
|
||||
statusHandler = object : ServiceStatusHandler(requireActivity()) {
|
||||
override fun loadMediaInfo() {
|
||||
setupAudioTracks()
|
||||
}
|
||||
}
|
||||
controller?.init()
|
||||
statusHandler?.init()
|
||||
}
|
||||
|
||||
@UnstableApi override fun onStop() {
|
||||
super.onStop()
|
||||
controller?.release()
|
||||
controller = null
|
||||
statusHandler?.release()
|
||||
statusHandler = null
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
_binding = AudioControlsBinding.inflate(layoutInflater)
|
||||
dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
|
@ -400,20 +397,17 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
.setPositiveButton(R.string.close_label, null).create()
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@UnstableApi private fun setupAudioTracks() {
|
||||
val butAudioTracks = binding.audioTracks
|
||||
if (audioTracks.size < 2 || selectedAudioTrack < 0) {
|
||||
butAudioTracks.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
butAudioTracks.visibility = View.VISIBLE
|
||||
butAudioTracks.text = audioTracks[selectedAudioTrack]
|
||||
butAudioTracks.setOnClickListener {
|
||||
|
|
|
@ -20,7 +20,7 @@ class MainActivityStarter(private val context: Context) {
|
|||
}
|
||||
|
||||
fun getIntent(): Intent {
|
||||
if (fragmentArgs != null) intent.putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs)
|
||||
if (fragmentArgs != null) intent.putExtra(Extras.fragment_args.name, fragmentArgs)
|
||||
return intent
|
||||
}
|
||||
|
||||
|
@ -33,32 +33,32 @@ class MainActivityStarter(private val context: Context) {
|
|||
}
|
||||
|
||||
fun withOpenPlayer(): MainActivityStarter {
|
||||
intent.putExtra(EXTRA_OPEN_PLAYER, true)
|
||||
intent.putExtra(Extras.open_player.name, true)
|
||||
return this
|
||||
}
|
||||
|
||||
fun withOpenFeed(feedId: Long): MainActivityStarter {
|
||||
intent.putExtra(EXTRA_FEED_ID, feedId)
|
||||
intent.putExtra(Extras.fragment_feed_id.name, feedId)
|
||||
return this
|
||||
}
|
||||
|
||||
fun withAddToBackStack(): MainActivityStarter {
|
||||
intent.putExtra(EXTRA_ADD_TO_BACK_STACK, true)
|
||||
intent.putExtra(Extras.add_to_back_stack.name, true)
|
||||
return this
|
||||
}
|
||||
|
||||
fun withFragmentLoaded(fragmentName: String?): MainActivityStarter {
|
||||
intent.putExtra(EXTRA_FRAGMENT_TAG, fragmentName)
|
||||
intent.putExtra(Extras.fragment_tag.name, fragmentName)
|
||||
return this
|
||||
}
|
||||
|
||||
fun withDrawerOpen(): MainActivityStarter {
|
||||
intent.putExtra(EXTRA_OPEN_DRAWER, true)
|
||||
intent.putExtra(Extras.open_drawer.name, true)
|
||||
return this
|
||||
}
|
||||
|
||||
fun withDownloadLogsOpen(): MainActivityStarter {
|
||||
intent.putExtra(EXTRA_OPEN_DOWNLOAD_LOGS, true)
|
||||
intent.putExtra(Extras.open_download_logs.name, true)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -69,14 +69,17 @@ class MainActivityStarter(private val context: Context) {
|
|||
return this
|
||||
}
|
||||
|
||||
@Suppress("EnumEntryName")
|
||||
enum class Extras {
|
||||
open_player,
|
||||
fragment_feed_id,
|
||||
add_to_back_stack,
|
||||
fragment_tag,
|
||||
open_drawer,
|
||||
open_download_logs,
|
||||
fragment_args
|
||||
}
|
||||
companion object {
|
||||
const val INTENT: String = "ac.mdiq.podcini.intents.MAIN_ACTIVITY"
|
||||
const val EXTRA_OPEN_PLAYER: String = "open_player"
|
||||
const val EXTRA_FEED_ID: String = "fragment_feed_id"
|
||||
const val EXTRA_ADD_TO_BACK_STACK: String = "add_to_back_stack"
|
||||
const val EXTRA_FRAGMENT_TAG: String = "fragment_tag"
|
||||
const val EXTRA_OPEN_DRAWER: String = "open_drawer"
|
||||
const val EXTRA_OPEN_DOWNLOAD_LOGS: String = "open_download_logs"
|
||||
const val EXTRA_FRAGMENT_ARGS: String = "fragment_args"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.activity.starter
|
||||
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Launches the playback speed dialog activity of the app with specific arguments.
|
||||
* Does not require a dependency on the actual implementation of the activity.
|
||||
*/
|
||||
class PlaybackSpeedActivityStarter(private val context: Context) {
|
||||
val intent: Intent = Intent(INTENT)
|
||||
|
||||
init {
|
||||
intent.setPackage(context.packageName)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_playback_speed, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
fun start() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INTENT: String = "ac.mdiq.podcini.intents.PLAYBACK_SPEED"
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import ac.mdiq.podcini.storage.model.MediaType
|
|||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.*
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment
|
||||
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.dialog
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FilterDialogBinding
|
||||
import ac.mdiq.podcini.databinding.FilterDialogRowBinding
|
||||
import ac.mdiq.podcini.storage.model.FeedFilter
|
||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.TAG
|
||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedsFilter
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class FeedFilterDialog : BottomSheetDialogFragment() {
|
||||
private lateinit var rows: LinearLayout
|
||||
private var _binding: FilterDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
var filter: FeedFilter? = null
|
||||
private val buttonMap: MutableMap<String, Button> = mutableMapOf()
|
||||
|
||||
private val newFilterValues: Set<String>
|
||||
get() {
|
||||
val newFilterValues: MutableSet<String> = HashSet()
|
||||
for (i in 0 until rows.childCount) {
|
||||
if (rows.getChildAt(i) !is MaterialButtonToggleGroup) continue
|
||||
val group = rows.getChildAt(i) as MaterialButtonToggleGroup
|
||||
if (group.checkedButtonId == View.NO_ID) continue
|
||||
val tag = group.findViewById<View>(group.checkedButtonId).tag as? String ?: continue
|
||||
newFilterValues.add(tag)
|
||||
}
|
||||
return newFilterValues
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val layout = inflater.inflate(R.layout.filter_dialog, container, false)
|
||||
_binding = FilterDialogBinding.bind(layout)
|
||||
rows = binding.filterRows
|
||||
Logd("FeedFilterDialog", "fragment onCreateView")
|
||||
|
||||
//add filter rows
|
||||
for (item in FeedFilterGroup.entries) {
|
||||
// Logd("EpisodeFilterDialog", "FeedItemFilterGroup: ${item.values[0].filterId} ${item.values[1].filterId}")
|
||||
val rBinding = FilterDialogRowBinding.inflate(inflater)
|
||||
// rowBinding.root.addOnButtonCheckedListener { _: MaterialButtonToggleGroup?, _: Int, _: Boolean ->
|
||||
// onFilterChanged(newFilterValues)
|
||||
// }
|
||||
rBinding.filterButton1.setOnClickListener { onFilterChanged(newFilterValues) }
|
||||
rBinding.filterButton2.setOnClickListener { onFilterChanged(newFilterValues) }
|
||||
|
||||
rBinding.filterButton1.setText(item.values[0].displayName)
|
||||
rBinding.filterButton1.tag = item.values[0].filterId
|
||||
buttonMap[item.values[0].filterId] = rBinding.filterButton1
|
||||
rBinding.filterButton2.setText(item.values[1].displayName)
|
||||
rBinding.filterButton2.tag = item.values[1].filterId
|
||||
buttonMap[item.values[1].filterId] = rBinding.filterButton2
|
||||
rBinding.filterButton1.maxLines = 3
|
||||
rBinding.filterButton1.isSingleLine = false
|
||||
rBinding.filterButton2.maxLines = 3
|
||||
rBinding.filterButton2.isSingleLine = false
|
||||
rows.addView(rBinding.root, rows.childCount - 1)
|
||||
}
|
||||
|
||||
binding.confirmFiltermenu.setOnClickListener { dismiss() }
|
||||
binding.resetFiltermenu.setOnClickListener {
|
||||
onFilterChanged(emptySet())
|
||||
for (i in 0 until rows.childCount) {
|
||||
if (rows.getChildAt(i) is MaterialButtonToggleGroup) (rows.getChildAt(i) as MaterialButtonToggleGroup).clearChecked()
|
||||
}
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
for (filterId in filter!!.values) {
|
||||
if (filterId.isNotEmpty()) {
|
||||
val button = buttonMap[filterId]
|
||||
if (button != null) (button.parent as MaterialButtonToggleGroup).check(button.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.setOnShowListener { dialogInterface: DialogInterface ->
|
||||
val bottomSheetDialog = dialogInterface as BottomSheetDialog
|
||||
setupFullHeight(bottomSheetDialog)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupFullHeight(bottomSheetDialog: BottomSheetDialog) {
|
||||
val bottomSheet = bottomSheetDialog.findViewById<View>(com.leinardi.android.speeddial.R.id.design_bottom_sheet) as? FrameLayout
|
||||
if (bottomSheet != null) {
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
val layoutParams = bottomSheet.layoutParams
|
||||
bottomSheet.layoutParams = layoutParams
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
feedsFilter = StringUtils.join(newFilterValues, ",")
|
||||
Logd(TAG, "onFilterChanged: $feedsFilter")
|
||||
EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues))
|
||||
}
|
||||
|
||||
enum class FeedFilterGroup(vararg values: ItemProperties) {
|
||||
KEEP_UPDATED(ItemProperties(R.string.keep_updated, FeedFilter.States.keepUpdated.name), ItemProperties(R.string.not_keep_updated, FeedFilter.States.not_keepUpdated.name)),
|
||||
PLAY_SPEED(ItemProperties(R.string.global_speed, FeedFilter.States.global_playSpeed.name), ItemProperties(R.string.custom_speed, FeedFilter.States.custom_playSpeed.name)),
|
||||
SKIPS(ItemProperties(R.string.has_skips, FeedFilter.States.has_skips.name), ItemProperties(R.string.no_skips, FeedFilter.States.no_skips.name)),
|
||||
AUTO_DELETE(ItemProperties(R.string.always_auto_delete, FeedFilter.States.always_auto_delete.name), ItemProperties(R.string.never_auto_delete, FeedFilter.States.never_auto_delete.name)),
|
||||
AUTO_DOWNLOAD(ItemProperties(R.string.auto_download, FeedFilter.States.autoDownload.name), ItemProperties(R.string.not_auto_download, FeedFilter.States.not_autoDownload.name));
|
||||
|
||||
@JvmField
|
||||
val values: Array<ItemProperties> = arrayOf(*values)
|
||||
|
||||
class ItemProperties(@JvmField val displayName: Int, @JvmField val filterId: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(filter: FeedFilter?): FeedFilterDialog {
|
||||
val dialog = FeedFilterDialog()
|
||||
dialog.filter = filter
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,8 @@ import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
|||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
@OptIn(UnstableApi::class) class SwipeActionsDialog(private val context: Context, private val tag: String) {
|
||||
@OptIn(UnstableApi::class)
|
||||
class SwipeActionsDialog(private val context: Context, private val tag: String) {
|
||||
private lateinit var keys: List<SwipeAction>
|
||||
|
||||
private var rightAction: SwipeAction? = null
|
||||
|
|
|
@ -45,7 +45,8 @@ import java.text.DecimalFormat
|
|||
import java.text.DecimalFormatSymbols
|
||||
import java.util.*
|
||||
|
||||
@OptIn(UnstableApi::class) open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
||||
@OptIn(UnstableApi::class)
|
||||
open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var adapter: SpeedSelectionAdapter
|
||||
private lateinit var speedSeekBar: PlaybackSpeedSeekBar
|
||||
|
|
|
@ -1,216 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.AddfeedBinding
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.net.feed.discovery.*
|
||||
import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.isOPMLRestared
|
||||
import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.performRestore
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||
import ac.mdiq.podcini.ui.fragment.NavDrawerFragment.Companion.feedCount
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Provides actions for adding new podcast subscriptions.
|
||||
*/
|
||||
@UnstableApi
|
||||
class AddFeedFragment : Fragment() {
|
||||
|
||||
private var _binding: AddfeedBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var activity: MainActivity? = null
|
||||
private var displayUpArrow = false
|
||||
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
this.chooseOpmlImportPathResult(uri) }
|
||||
|
||||
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) { uri: Uri? -> this.addLocalFolderResult(uri) }
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
_binding = AddfeedBinding.inflate(inflater)
|
||||
activity = getActivity() as? MainActivity
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
|
||||
(getActivity() as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||
|
||||
binding.searchVistaGuideButton.setOnClickListener { activity?.loadChildFragment(OnlineSearchFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
|
||||
binding.searchItunesButton.setOnClickListener { activity?.loadChildFragment(OnlineSearchFragment.newInstance(ItunesPodcastSearcher::class.java)) }
|
||||
binding.searchFyydButton.setOnClickListener { activity?.loadChildFragment(OnlineSearchFragment.newInstance(FyydPodcastSearcher::class.java)) }
|
||||
binding.searchGPodderButton.setOnClickListener { activity?.loadChildFragment(OnlineSearchFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
|
||||
binding.searchPodcastIndexButton.setOnClickListener { activity?.loadChildFragment(OnlineSearchFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
|
||||
binding.combinedFeedSearchEditText.setOnEditorActionListener { _: TextView?, _: Int, _: KeyEvent? ->
|
||||
performSearch()
|
||||
true
|
||||
}
|
||||
binding.addViaUrlButton.setOnClickListener { showAddViaUrlDialog() }
|
||||
binding.opmlImportButton.setOnClickListener {
|
||||
try {
|
||||
chooseOpmlImportPathLauncher.launch("*/*")
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
binding.addLocalFolderButton.setOnClickListener {
|
||||
try {
|
||||
addLocalFolderLauncher.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
binding.searchButton.setOnClickListener { performSearch() }
|
||||
if (isOPMLRestared && feedCount == 0) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.restore_subscriptions_label)
|
||||
.setMessage(R.string.restore_subscriptions_summary)
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
performRestore(requireContext())
|
||||
dialog.dismiss()
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_UP_ARROW, displayUpArrow)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun showAddViaUrlDialog() {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.add_podcast_by_url)
|
||||
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
|
||||
dialogBinding.editText.setHint(R.string.add_podcast_by_url_hint)
|
||||
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clipData: ClipData? = clipboard.primaryClip
|
||||
if (clipData != null && clipData.itemCount > 0 && clipData.getItemAt(0).text != null) {
|
||||
val clipboardContent: String = clipData.getItemAt(0).text.toString()
|
||||
if (clipboardContent.trim { it <= ' ' }.startsWith("http")) dialogBinding.editText.setText(clipboardContent.trim { it <= ' ' })
|
||||
}
|
||||
builder.setView(dialogBinding.root)
|
||||
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> addUrl(dialogBinding.editText.text.toString()) }
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun addUrl(url: String) {
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(url)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
private fun performSearch() {
|
||||
binding.combinedFeedSearchEditText.clearFocus()
|
||||
val inVal = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inVal.hideSoftInputFromWindow(binding.combinedFeedSearchEditText.windowToken, 0)
|
||||
val query = binding.combinedFeedSearchEditText.text.toString()
|
||||
if (query.matches("http[s]?://.*".toRegex())) {
|
||||
addUrl(query)
|
||||
return
|
||||
}
|
||||
activity?.loadChildFragment(OnlineSearchFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
binding.combinedFeedSearchEditText.post { binding.combinedFeedSearchEditText.setText("") }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun chooseOpmlImportPathResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
|
||||
val intent = Intent(context, OpmlImportActivity::class.java)
|
||||
intent.setData(uri)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@UnstableApi private fun addLocalFolderResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
val feed = withContext(Dispatchers.IO) { addLocalFolder(uri) }
|
||||
withContext(Dispatchers.Main) {
|
||||
if (feed != null) {
|
||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||
(getActivity() as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
(getActivity() as MainActivity).showSnackbarAbovePlayer(e.localizedMessage, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun addLocalFolder(uri: Uri): Feed? {
|
||||
requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||
requireNotNull(documentFile) { "Unable to retrieve document tree" }
|
||||
var title = documentFile.name
|
||||
if (title == null) title = getString(R.string.local_folder)
|
||||
|
||||
val dirFeed = Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title)
|
||||
dirFeed.episodes.clear()
|
||||
dirFeed.sortOrder = EpisodeSortOrder.EPISODE_TITLE_A_Z
|
||||
val fromDatabase: Feed? = updateFeed(requireContext(), dirFeed, false)
|
||||
FeedUpdateManager.runOnce(requireContext(), fromDatabase)
|
||||
return fromDatabase
|
||||
}
|
||||
|
||||
private class AddLocalFolder : ActivityResultContracts.OpenDocumentTree() {
|
||||
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||
return super.createIntent(context, input).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = AddFeedFragment::class.simpleName ?: "Anonymous"
|
||||
private const val KEY_UP_ARROW = "up_arrow"
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ import ac.mdiq.podcini.storage.model.Playable
|
|||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
|
||||
|
|
|
@ -6,7 +6,7 @@ import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
|||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||
|
|
|
@ -1,274 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FragmentOnlineSearchBinding
|
||||
import ac.mdiq.podcini.databinding.SelectCountryDialogBinding
|
||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
|
||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader.Companion.prefs
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.OnFocusChangeListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Searches iTunes store for top podcasts and displays results in a list.
|
||||
*/
|
||||
class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
private var _binding: FragmentOnlineSearchBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
// private lateinit var prefs: SharedPreferences
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var txtvError: TextView
|
||||
private lateinit var butRetry: Button
|
||||
private lateinit var txtvEmpty: TextView
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
/**
|
||||
* Adapter responsible with the search results.
|
||||
*/
|
||||
private var adapter: OnlineFeedsAdapter? = null
|
||||
|
||||
/**
|
||||
* List of podcasts retreived from the search.
|
||||
*/
|
||||
private var searchResults: List<PodcastSearchResult>? = null
|
||||
private var topList: List<PodcastSearchResult>? = null
|
||||
|
||||
private var countryCode: String? = "US"
|
||||
private var hidden = false
|
||||
private var needsConfirm = false
|
||||
|
||||
/**
|
||||
* Replace adapter data with provided search results from SearchTask.
|
||||
*
|
||||
* @param result List of Podcast objects containing search results
|
||||
*/
|
||||
private fun updateData(result: List<PodcastSearchResult>?) {
|
||||
this.searchResults = result
|
||||
adapter?.clear()
|
||||
if (!result.isNullOrEmpty()) {
|
||||
gridView.visibility = View.VISIBLE
|
||||
txtvEmpty.visibility = View.GONE
|
||||
for (p in result) {
|
||||
adapter!!.add(p)
|
||||
}
|
||||
adapter?.notifyDataSetInvalidated()
|
||||
} else {
|
||||
gridView.visibility = View.GONE
|
||||
txtvEmpty.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// prefs = requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE)
|
||||
countryCode = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)
|
||||
hidden = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)
|
||||
needsConfirm = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
// Inflate the layout for this fragment
|
||||
_binding = FragmentOnlineSearchBinding.inflate(inflater)
|
||||
// val root = inflater.inflate(R.layout.fragment_itunes_search, container, false)
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
gridView = binding.gridView
|
||||
adapter = OnlineFeedsAdapter(requireActivity(), ArrayList())
|
||||
gridView.setAdapter(adapter)
|
||||
|
||||
toolbar = binding.toolbar
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.inflateMenu(R.menu.countries_menu)
|
||||
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
|
||||
discoverHideItem.setChecked(hidden)
|
||||
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
//Show information about the podcast when the list item is clicked
|
||||
gridView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
val podcast = searchResults!![position]
|
||||
if (podcast.feedUrl == null) return@OnItemClickListener
|
||||
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
progressBar = binding.progressBar
|
||||
txtvError = binding.txtvError
|
||||
butRetry = binding.butRetry
|
||||
txtvEmpty = binding.empty
|
||||
|
||||
loadToplist(countryCode)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
adapter = null
|
||||
searchResults = null
|
||||
topList = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun loadToplist(country: String?) {
|
||||
gridView.visibility = View.GONE
|
||||
txtvError.visibility = View.GONE
|
||||
butRetry.visibility = View.GONE
|
||||
butRetry.setText(R.string.retry_label)
|
||||
txtvEmpty.visibility = View.GONE
|
||||
progressBar.visibility = View.VISIBLE
|
||||
|
||||
if (hidden) {
|
||||
gridView.visibility = View.GONE
|
||||
txtvError.visibility = View.VISIBLE
|
||||
txtvError.text = resources.getString(R.string.discover_is_hidden)
|
||||
butRetry.visibility = View.GONE
|
||||
txtvEmpty.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
if (BuildConfig.FLAVOR == "free" && needsConfirm) {
|
||||
txtvError.visibility = View.VISIBLE
|
||||
txtvError.text = ""
|
||||
butRetry.visibility = View.VISIBLE
|
||||
butRetry.setText(R.string.discover_confirm)
|
||||
butRetry.setOnClickListener {
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
|
||||
needsConfirm = false
|
||||
loadToplist(country)
|
||||
}
|
||||
txtvEmpty.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
val loader = ItunesTopListLoader(requireContext())
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val podcasts = withContext(Dispatchers.IO) {
|
||||
loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
progressBar.visibility = View.GONE
|
||||
topList = podcasts
|
||||
updateData(topList)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
progressBar.visibility = View.GONE
|
||||
txtvError.text = e.message
|
||||
txtvError.visibility = View.VISIBLE
|
||||
butRetry.setOnClickListener { loadToplist(country) }
|
||||
butRetry.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (super.onOptionsItemSelected(item)) return true
|
||||
|
||||
val itemId = item.itemId
|
||||
when (itemId) {
|
||||
R.id.discover_hide_item -> {
|
||||
item.setChecked(!item.isChecked)
|
||||
hidden = item.isChecked
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
|
||||
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
|
||||
loadToplist(countryCode)
|
||||
return true
|
||||
}
|
||||
R.id.discover_countries_item -> {
|
||||
val inflater = layoutInflater
|
||||
val selectCountryDialogView = inflater.inflate(R.layout.select_country_dialog, null)
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setView(selectCountryDialogView)
|
||||
|
||||
val countryCodeArray: List<String> = listOf(*Locale.getISOCountries())
|
||||
val countryCodeNames: MutableMap<String?, String> = HashMap()
|
||||
val countryNameCodes: MutableMap<String, String> = HashMap()
|
||||
for (code in countryCodeArray) {
|
||||
val locale = Locale("", code)
|
||||
val countryName = locale.displayCountry
|
||||
countryCodeNames[code] = countryName
|
||||
countryNameCodes[countryName] = code
|
||||
}
|
||||
|
||||
val countryNamesSort: MutableList<String> = ArrayList(countryCodeNames.values)
|
||||
countryNamesSort.sort()
|
||||
|
||||
val dataAdapter = ArrayAdapter(this.requireContext(), android.R.layout.simple_list_item_1, countryNamesSort)
|
||||
val scBinding = SelectCountryDialogBinding.bind(selectCountryDialogView)
|
||||
val textInput = scBinding.countryTextInput
|
||||
val editText = textInput.editText as? MaterialAutoCompleteTextView
|
||||
editText!!.setAdapter(dataAdapter)
|
||||
editText.setText(countryCodeNames[countryCode])
|
||||
editText.setOnClickListener {
|
||||
if (editText.text.isNotEmpty()) {
|
||||
editText.setText("")
|
||||
editText.postDelayed({ editText.showDropDown() }, 100)
|
||||
}
|
||||
}
|
||||
editText.onFocusChangeListener = OnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
||||
if (hasFocus) {
|
||||
editText.setText("")
|
||||
editText.postDelayed({ editText.showDropDown() }, 100)
|
||||
}
|
||||
}
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
val countryName = editText.text.toString()
|
||||
if (countryNameCodes.containsKey(countryName)) {
|
||||
countryCode = countryNameCodes[countryName]
|
||||
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
|
||||
discoverHideItem.setChecked(false)
|
||||
hidden = false
|
||||
}
|
||||
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
prefs!!.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply()
|
||||
|
||||
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
|
||||
loadToplist(countryCode)
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.show()
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = DiscoveryFragment::class.simpleName ?: "Anonymous"
|
||||
private const val NUM_OF_TOP_PODCASTS = 25
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ import ac.mdiq.podcini.storage.model.EpisodeFilter
|
|||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
|
|
|
@ -16,7 +16,7 @@ import ac.mdiq.podcini.storage.model.*
|
|||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||
|
|
|
@ -116,7 +116,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
|
||||
binding.btnvRelatedFeeds.setOnClickListener {
|
||||
val fragment = OnlineSearchFragment.newInstance(CombinedSearcher::class.java, "${binding.header.txtvAuthor.text} podcasts")
|
||||
val fragment = SearchResultsFragment.newInstance(CombinedSearcher::class.java, "${binding.header.txtvAuthor.text} podcasts")
|
||||
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||
}
|
||||
binding.txtvUrl.setOnClickListener(copyUrlToClipboard)
|
||||
|
|
|
@ -186,7 +186,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
|||
HistoryFragment.TAG -> R.drawable.ic_history
|
||||
SubscriptionsFragment.TAG -> R.drawable.ic_subscriptions
|
||||
StatisticsFragment.TAG -> R.drawable.ic_chart_box
|
||||
AddFeedFragment.TAG -> R.drawable.ic_add
|
||||
OnlineSearchFragment.TAG -> R.drawable.ic_add
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
@ -384,7 +384,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
|||
DownloadsFragment.TAG,
|
||||
HistoryFragment.TAG,
|
||||
StatisticsFragment.TAG,
|
||||
AddFeedFragment.TAG,
|
||||
OnlineSearchFragment.TAG,
|
||||
)
|
||||
|
||||
fun saveLastNavFragment(tag: String?) {
|
||||
|
|
|
@ -94,6 +94,7 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
|
||||
var feedSource: String = ""
|
||||
var feedUrl: String = ""
|
||||
|
||||
private val feedId: Long
|
||||
get() {
|
||||
if (feeds == null) return 0
|
||||
|
@ -128,6 +129,7 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||
|
||||
feedUrl = requireArguments().getString(ARG_FEEDURL) ?: ""
|
||||
Logd(TAG, "feedUrl: $feedUrl")
|
||||
if (feedUrl.isEmpty()) {
|
||||
Log.e(TAG, "feedUrl is null.")
|
||||
showNoPodcastFoundError()
|
||||
|
@ -194,8 +196,7 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
private fun lookupUrlAndBuild(url: String) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
||||
try {
|
||||
startFeedBuilding(urlString)
|
||||
try { startFeedBuilding(urlString)
|
||||
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
|
@ -258,7 +259,8 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
|
||||
private fun startFeedBuilding(url: String) {
|
||||
Logd(TAG, "startFeedBuilding")
|
||||
if (feedSource == "VistaGuide") {
|
||||
if (feedSource == "VistaGuide" || url.contains("youtube.com")) {
|
||||
feedSource = "VistaGuide"
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
feeds = getFeedList()
|
||||
|
|
|
@ -1,198 +1,210 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FragmentOnlineSearchBinding
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcher
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.databinding.AddfeedBinding
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.net.feed.discovery.*
|
||||
import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.isOPMLRestared
|
||||
import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.performRestore
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
|
||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||
import ac.mdiq.podcini.ui.fragment.NavDrawerFragment.Companion.feedCount
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Provides actions for adding new podcast subscriptions.
|
||||
*/
|
||||
@UnstableApi
|
||||
class OnlineSearchFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentOnlineSearchBinding? = null
|
||||
private var _binding: AddfeedBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var adapter: OnlineFeedsAdapter? = null
|
||||
private var searchProvider: PodcastSearcher? = null
|
||||
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var txtvError: TextView
|
||||
private lateinit var butRetry: Button
|
||||
private lateinit var txtvEmpty: TextView
|
||||
private var activity: MainActivity? = null
|
||||
private var displayUpArrow = false
|
||||
|
||||
/**
|
||||
* List of podcasts retreived from the search
|
||||
*/
|
||||
private var searchResults: MutableList<PodcastSearchResult>? = null
|
||||
// private var disposable: Disposable? = null
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
this.chooseOpmlImportPathResult(uri) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
for (info in PodcastSearcherRegistry.searchProviders) {
|
||||
Logd(TAG, "searchProvider: $info")
|
||||
if (info.searcher.javaClass.getName() == requireArguments().getString(ARG_SEARCHER)) {
|
||||
searchProvider = info.searcher
|
||||
break
|
||||
}
|
||||
}
|
||||
if (searchProvider == null) Logd(TAG,"Podcast searcher not found")
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentOnlineSearchBinding.inflate(inflater)
|
||||
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) { uri: Uri? -> this.addLocalFolderResult(uri) }
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
_binding = AddfeedBinding.inflate(inflater)
|
||||
activity = getActivity() as? MainActivity
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
gridView = binding.gridView
|
||||
adapter = OnlineFeedsAdapter(requireContext(), ArrayList())
|
||||
gridView.setAdapter(adapter)
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
(getActivity() as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||
|
||||
//Show information about the podcast when the list item is clicked
|
||||
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
val podcast = searchResults!![position]
|
||||
if (podcast.feedUrl != null) {
|
||||
val fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
fragment.feedSource = podcast.source
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
binding.searchButton.setOnClickListener { performSearch() }
|
||||
binding.searchVistaGuideButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
|
||||
binding.searchItunesButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }
|
||||
binding.searchFyydButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }
|
||||
binding.searchGPodderButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
|
||||
binding.searchPodcastIndexButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
|
||||
binding.combinedFeedSearchEditText.setOnEditorActionListener { _: TextView?, _: Int, _: KeyEvent? ->
|
||||
performSearch()
|
||||
true
|
||||
}
|
||||
binding.addViaUrlButton.setOnClickListener { showAddViaUrlDialog() }
|
||||
binding.opmlImportButton.setOnClickListener {
|
||||
try { chooseOpmlImportPathLauncher.launch("*/*")
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
progressBar = binding.progressBar
|
||||
txtvError = binding.txtvError
|
||||
butRetry = binding.butRetry
|
||||
txtvEmpty = binding.empty
|
||||
if (searchProvider != null) binding.searchPoweredBy.text = getString(R.string.search_powered_by, searchProvider!!.name)
|
||||
setupToolbar(binding.toolbar)
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
|
||||
val imm = activity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
binding.addLocalFolderButton.setOnClickListener {
|
||||
try { addLocalFolderLauncher.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {}
|
||||
})
|
||||
}
|
||||
|
||||
if (isOPMLRestared && feedCount == 0) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.restore_subscriptions_label)
|
||||
.setMessage(R.string.restore_subscriptions_summary)
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
performRestore(requireContext())
|
||||
dialog.dismiss()
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_UP_ARROW, displayUpArrow)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun showAddViaUrlDialog() {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.add_podcast_by_url)
|
||||
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
|
||||
dialogBinding.editText.setHint(R.string.add_podcast_by_url_hint)
|
||||
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clipData: ClipData? = clipboard.primaryClip
|
||||
if (clipData != null && clipData.itemCount > 0 && clipData.getItemAt(0).text != null) {
|
||||
val clipboardContent: String = clipData.getItemAt(0).text.toString()
|
||||
if (clipboardContent.trim { it <= ' ' }.startsWith("http")) dialogBinding.editText.setText(clipboardContent.trim { it <= ' ' })
|
||||
}
|
||||
builder.setView(dialogBinding.root)
|
||||
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> addUrl(dialogBinding.editText.text.toString()) }
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun addUrl(url: String) {
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(url)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
private fun performSearch() {
|
||||
binding.combinedFeedSearchEditText.clearFocus()
|
||||
val inVal = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inVal.hideSoftInputFromWindow(binding.combinedFeedSearchEditText.windowToken, 0)
|
||||
val query = binding.combinedFeedSearchEditText.text.toString()
|
||||
if (query.matches("http[s]?://.*".toRegex())) {
|
||||
addUrl(query)
|
||||
return
|
||||
}
|
||||
activity?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
binding.combinedFeedSearchEditText.post { binding.combinedFeedSearchEditText.setText("") }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
searchResults = null
|
||||
adapter = null
|
||||
super.onDestroy()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupToolbar(toolbar: MaterialToolbar) {
|
||||
toolbar.inflateMenu(R.menu.online_search)
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
private fun chooseOpmlImportPathResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
|
||||
val searchItem: MenuItem = toolbar.menu.findItem(R.id.action_search)
|
||||
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()) })
|
||||
}
|
||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
requireActivity().supportFragmentManager.popBackStack()
|
||||
return true
|
||||
}
|
||||
})
|
||||
searchItem.expandActionView()
|
||||
if (requireArguments().getString(ARG_QUERY, null) != null)
|
||||
sv?.setQuery(requireArguments().getString(ARG_QUERY, null), true)
|
||||
val intent = Intent(context, OpmlImportActivity::class.java)
|
||||
intent.setData(uri)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
showOnlyProgressBar()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@UnstableApi private fun addLocalFolderResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
val result = searchProvider?.search(query)
|
||||
searchResults = result?.toMutableList()
|
||||
val feed = withContext(Dispatchers.IO) { addLocalFolder(uri) }
|
||||
withContext(Dispatchers.Main) {
|
||||
progressBar.visibility = View.GONE
|
||||
adapter?.clear()
|
||||
handleSearchResults()
|
||||
txtvEmpty.text = getString(R.string.no_results_for_query, query)
|
||||
if (feed != null) {
|
||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||
(getActivity() as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { handleSearchError(e, query) }
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
(getActivity() as MainActivity).showSnackbarAbovePlayer(e.localizedMessage, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSearchResults() {
|
||||
adapter?.addAll(searchResults!!)
|
||||
adapter?.notifyDataSetInvalidated()
|
||||
gridView.visibility = if (!searchResults.isNullOrEmpty()) View.VISIBLE else View.GONE
|
||||
txtvEmpty.visibility = if (searchResults.isNullOrEmpty()) View.VISIBLE else View.GONE
|
||||
@UnstableApi private fun addLocalFolder(uri: Uri): Feed? {
|
||||
requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||
requireNotNull(documentFile) { "Unable to retrieve document tree" }
|
||||
var title = documentFile.name
|
||||
if (title == null) title = getString(R.string.local_folder)
|
||||
|
||||
val dirFeed = Feed(Feed.PREFIX_LOCAL_FOLDER + uri.toString(), null, title)
|
||||
dirFeed.episodes.clear()
|
||||
dirFeed.sortOrder = EpisodeSortOrder.EPISODE_TITLE_A_Z
|
||||
val fromDatabase: Feed? = updateFeed(requireContext(), dirFeed, false)
|
||||
FeedUpdateManager.runOnce(requireContext(), fromDatabase)
|
||||
return fromDatabase
|
||||
}
|
||||
|
||||
private fun handleSearchError(e: Throwable, query: String) {
|
||||
Logd(TAG, "exception: ${e.message}")
|
||||
progressBar.visibility = View.GONE
|
||||
txtvError.text = e.toString()
|
||||
txtvError.visibility = View.VISIBLE
|
||||
butRetry.setOnClickListener { search(query) }
|
||||
butRetry.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showOnlyProgressBar() {
|
||||
gridView.visibility = View.GONE
|
||||
txtvError.visibility = View.GONE
|
||||
butRetry.visibility = View.GONE
|
||||
txtvEmpty.visibility = View.GONE
|
||||
progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showInputMethod(view: View) {
|
||||
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(view, 0)
|
||||
private class AddLocalFolder : ActivityResultContracts.OpenDocumentTree() {
|
||||
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||
return super.createIntent(context, input).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = OnlineSearchFragment::class.simpleName ?: "Anonymous"
|
||||
private const val ARG_SEARCHER = "searcher"
|
||||
private const val ARG_QUERY = "query"
|
||||
|
||||
@JvmOverloads
|
||||
fun newInstance(searchProvider: Class<out PodcastSearcher?>, query: String? = null): OnlineSearchFragment {
|
||||
val fragment = OnlineSearchFragment()
|
||||
val arguments = Bundle()
|
||||
arguments.putString(ARG_SEARCHER, searchProvider.name)
|
||||
arguments.putString(ARG_QUERY, query)
|
||||
fragment.arguments = arguments
|
||||
return fragment
|
||||
}
|
||||
val TAG = OnlineSearchFragment::class.simpleName ?: "Anonymous"
|
||||
private const val KEY_UP_ARROW = "up_arrow"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import ac.mdiq.podcini.storage.model.PlayQueue
|
|||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||
|
|
|
@ -2,28 +2,37 @@ package ac.mdiq.podcini.ui.fragment
|
|||
|
||||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FragmentSearchResultsBinding
|
||||
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryBinding
|
||||
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryItemBinding
|
||||
import ac.mdiq.podcini.databinding.SelectCountryDialogBinding
|
||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
|
||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader.Companion.prefs
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import coil.load
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
@ -227,6 +236,245 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches iTunes store for top podcasts and displays results in a list.
|
||||
*/
|
||||
class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
private var _binding: FragmentSearchResultsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
// private lateinit var prefs: SharedPreferences
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var txtvError: TextView
|
||||
private lateinit var butRetry: Button
|
||||
private lateinit var txtvEmpty: TextView
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
/**
|
||||
* Adapter responsible with the search results.
|
||||
*/
|
||||
private var adapter: OnlineFeedsAdapter? = null
|
||||
|
||||
/**
|
||||
* List of podcasts retreived from the search.
|
||||
*/
|
||||
private var searchResults: List<PodcastSearchResult>? = null
|
||||
private var topList: List<PodcastSearchResult>? = null
|
||||
|
||||
private var countryCode: String? = "US"
|
||||
private var hidden = false
|
||||
private var needsConfirm = false
|
||||
|
||||
/**
|
||||
* Replace adapter data with provided search results from SearchTask.
|
||||
*
|
||||
* @param result List of Podcast objects containing search results
|
||||
*/
|
||||
private fun updateData(result: List<PodcastSearchResult>?) {
|
||||
this.searchResults = result
|
||||
adapter?.clear()
|
||||
if (!result.isNullOrEmpty()) {
|
||||
gridView.visibility = View.VISIBLE
|
||||
txtvEmpty.visibility = View.GONE
|
||||
for (p in result) {
|
||||
adapter!!.add(p)
|
||||
}
|
||||
adapter?.notifyDataSetInvalidated()
|
||||
} else {
|
||||
gridView.visibility = View.GONE
|
||||
txtvEmpty.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// prefs = requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE)
|
||||
countryCode = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)
|
||||
hidden = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)
|
||||
needsConfirm = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
// Inflate the layout for this fragment
|
||||
_binding = FragmentSearchResultsBinding.inflate(inflater)
|
||||
// val root = inflater.inflate(R.layout.fragment_itunes_search, container, false)
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
gridView = binding.gridView
|
||||
adapter = OnlineFeedsAdapter(requireActivity(), ArrayList())
|
||||
gridView.setAdapter(adapter)
|
||||
|
||||
toolbar = binding.toolbar
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.inflateMenu(R.menu.countries_menu)
|
||||
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
|
||||
discoverHideItem.setChecked(hidden)
|
||||
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
//Show information about the podcast when the list item is clicked
|
||||
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
val podcast = searchResults!![position]
|
||||
if (podcast.feedUrl == null) return@OnItemClickListener
|
||||
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
progressBar = binding.progressBar
|
||||
txtvError = binding.txtvError
|
||||
butRetry = binding.butRetry
|
||||
txtvEmpty = binding.empty
|
||||
|
||||
loadToplist(countryCode)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
adapter = null
|
||||
searchResults = null
|
||||
topList = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun loadToplist(country: String?) {
|
||||
gridView.visibility = View.GONE
|
||||
txtvError.visibility = View.GONE
|
||||
butRetry.visibility = View.GONE
|
||||
butRetry.setText(R.string.retry_label)
|
||||
txtvEmpty.visibility = View.GONE
|
||||
progressBar.visibility = View.VISIBLE
|
||||
|
||||
if (hidden) {
|
||||
gridView.visibility = View.GONE
|
||||
txtvError.visibility = View.VISIBLE
|
||||
txtvError.text = resources.getString(R.string.discover_is_hidden)
|
||||
butRetry.visibility = View.GONE
|
||||
txtvEmpty.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
if (BuildConfig.FLAVOR == "free" && needsConfirm) {
|
||||
txtvError.visibility = View.VISIBLE
|
||||
txtvError.text = ""
|
||||
butRetry.visibility = View.VISIBLE
|
||||
butRetry.setText(R.string.discover_confirm)
|
||||
butRetry.setOnClickListener {
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
|
||||
needsConfirm = false
|
||||
loadToplist(country)
|
||||
}
|
||||
txtvEmpty.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
val loader = ItunesTopListLoader(requireContext())
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val podcasts = withContext(Dispatchers.IO) {
|
||||
loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
progressBar.visibility = View.GONE
|
||||
topList = podcasts
|
||||
updateData(topList)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
progressBar.visibility = View.GONE
|
||||
txtvError.text = e.message
|
||||
txtvError.visibility = View.VISIBLE
|
||||
butRetry.setOnClickListener { loadToplist(country) }
|
||||
butRetry.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (super.onOptionsItemSelected(item)) return true
|
||||
|
||||
val itemId = item.itemId
|
||||
when (itemId) {
|
||||
R.id.discover_hide_item -> {
|
||||
item.setChecked(!item.isChecked)
|
||||
hidden = item.isChecked
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
|
||||
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
|
||||
loadToplist(countryCode)
|
||||
return true
|
||||
}
|
||||
R.id.discover_countries_item -> {
|
||||
val inflater = layoutInflater
|
||||
val selectCountryDialogView = inflater.inflate(R.layout.select_country_dialog, null)
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setView(selectCountryDialogView)
|
||||
|
||||
val countryCodeArray: List<String> = listOf(*Locale.getISOCountries())
|
||||
val countryCodeNames: MutableMap<String?, String> = HashMap()
|
||||
val countryNameCodes: MutableMap<String, String> = HashMap()
|
||||
for (code in countryCodeArray) {
|
||||
val locale = Locale("", code)
|
||||
val countryName = locale.displayCountry
|
||||
countryCodeNames[code] = countryName
|
||||
countryNameCodes[countryName] = code
|
||||
}
|
||||
|
||||
val countryNamesSort: MutableList<String> = ArrayList(countryCodeNames.values)
|
||||
countryNamesSort.sort()
|
||||
|
||||
val dataAdapter = ArrayAdapter(this.requireContext(), android.R.layout.simple_list_item_1, countryNamesSort)
|
||||
val scBinding = SelectCountryDialogBinding.bind(selectCountryDialogView)
|
||||
val textInput = scBinding.countryTextInput
|
||||
val editText = textInput.editText as? MaterialAutoCompleteTextView
|
||||
editText!!.setAdapter(dataAdapter)
|
||||
editText.setText(countryCodeNames[countryCode])
|
||||
editText.setOnClickListener {
|
||||
if (editText.text.isNotEmpty()) {
|
||||
editText.setText("")
|
||||
editText.postDelayed({ editText.showDropDown() }, 100)
|
||||
}
|
||||
}
|
||||
editText.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
||||
if (hasFocus) {
|
||||
editText.setText("")
|
||||
editText.postDelayed({ editText.showDropDown() }, 100)
|
||||
}
|
||||
}
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
val countryName = editText.text.toString()
|
||||
if (countryNameCodes.containsKey(countryName)) {
|
||||
countryCode = countryNameCodes[countryName]
|
||||
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
|
||||
discoverHideItem.setChecked(false)
|
||||
hidden = false
|
||||
}
|
||||
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
prefs!!.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply()
|
||||
|
||||
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
|
||||
loadToplist(countryCode)
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.show()
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = DiscoveryFragment::class.simpleName ?: "Anonymous"
|
||||
private const val NUM_OF_TOP_PODCASTS = 25
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = QuickDiscoveryFragment::class.simpleName ?: "Anonymous"
|
||||
private const val NUM_SUGGESTIONS = 12
|
||||
|
|
|
@ -10,9 +10,9 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
|
|||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
|
@ -26,6 +26,7 @@ import ac.mdiq.podcini.ui.view.SquareImageView
|
|||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
|
@ -63,7 +64,8 @@ import java.lang.ref.WeakReference
|
|||
/**
|
||||
* Performs a search operation on all feeds or one specific feed and displays the search result.
|
||||
*/
|
||||
@UnstableApi class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
@UnstableApi
|
||||
class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
private var _binding: SearchFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
|
@ -290,15 +292,14 @@ import java.lang.ref.WeakReference
|
|||
search()
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
@UnstableApi private fun search() {
|
||||
adapterFeeds.setEndButton(R.string.search_online) { this.searchOnline() }
|
||||
chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
performSearch()
|
||||
}
|
||||
val results = withContext(Dispatchers.IO) { performSearch() }
|
||||
withContext(Dispatchers.Main) {
|
||||
progressBar.visibility = View.GONE
|
||||
if (results.first != null) {
|
||||
|
@ -398,7 +399,7 @@ import java.lang.ref.WeakReference
|
|||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
return
|
||||
}
|
||||
(activity as MainActivity).loadChildFragment(OnlineSearchFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
(activity as MainActivity).loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
}
|
||||
|
||||
override fun onStartSelectMode() {
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FragmentSearchResultsBinding
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcher
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SearchResultsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentSearchResultsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var adapter: OnlineFeedsAdapter? = null
|
||||
private var searchProvider: PodcastSearcher? = null
|
||||
private lateinit var gridView: GridView
|
||||
|
||||
private var searchResults: MutableList<PodcastSearchResult> = mutableListOf()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
for (info in PodcastSearcherRegistry.searchProviders) {
|
||||
Logd(TAG, "searchProvider: $info")
|
||||
if (info.searcher.javaClass.getName() == requireArguments().getString(ARG_SEARCHER)) {
|
||||
searchProvider = info.searcher
|
||||
break
|
||||
}
|
||||
}
|
||||
if (searchProvider == null) Logd(TAG,"Podcast searcher not found")
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentSearchResultsBinding.inflate(inflater)
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
gridView = binding.gridView
|
||||
adapter = OnlineFeedsAdapter(requireContext(), ArrayList())
|
||||
gridView.setAdapter(adapter)
|
||||
|
||||
//Show information about the podcast when the list item is clicked
|
||||
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
val podcast = searchResults[position]
|
||||
if (podcast.feedUrl != null) {
|
||||
val fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
fragment.feedSource = podcast.source
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
if (searchProvider != null) binding.searchPoweredBy.text = getString(R.string.search_powered_by, searchProvider!!.name)
|
||||
setupToolbar(binding.toolbar)
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
|
||||
val imm = activity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
}
|
||||
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {}
|
||||
})
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
searchResults = mutableListOf()
|
||||
adapter = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun setupToolbar(toolbar: MaterialToolbar) {
|
||||
toolbar.inflateMenu(R.menu.online_search)
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
|
||||
val searchItem: MenuItem = toolbar.menu.findItem(R.id.action_search)
|
||||
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()) })
|
||||
}
|
||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
requireActivity().supportFragmentManager.popBackStack()
|
||||
return true
|
||||
}
|
||||
})
|
||||
searchItem.expandActionView()
|
||||
if (requireArguments().getString(ARG_QUERY, null) != null)
|
||||
sv?.setQuery(requireArguments().getString(ARG_QUERY, null), true)
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun search(query: String) {
|
||||
showOnlyProgressBar()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val result = searchProvider?.search(query) ?: listOf()
|
||||
searchResults = result.toMutableList()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
adapter?.clear()
|
||||
handleSearchResults()
|
||||
binding.empty.text = getString(R.string.no_results_for_query, query)
|
||||
}
|
||||
} catch (e: Exception) { handleSearchError(e, query) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSearchResults() {
|
||||
adapter?.addAll(searchResults)
|
||||
adapter?.notifyDataSetInvalidated()
|
||||
gridView.visibility = if (searchResults.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
binding.empty.visibility = if (searchResults.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun handleSearchError(e: Throwable, query: String) {
|
||||
Logd(TAG, "exception: ${e.message}")
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.txtvError.text = e.toString()
|
||||
binding.txtvError.visibility = View.VISIBLE
|
||||
binding.butRetry.setOnClickListener { search(query) }
|
||||
binding.butRetry.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showOnlyProgressBar() {
|
||||
gridView.visibility = View.GONE
|
||||
binding.txtvError.visibility = View.GONE
|
||||
binding.butRetry.visibility = View.GONE
|
||||
binding.empty.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showInputMethod(view: View) {
|
||||
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(view, 0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = SearchResultsFragment::class.simpleName ?: "Anonymous"
|
||||
private const val ARG_SEARCHER = "searcher"
|
||||
private const val ARG_QUERY = "query"
|
||||
|
||||
@JvmOverloads
|
||||
fun newInstance(searchProvider: Class<out PodcastSearcher?>, query: String? = null): SearchResultsFragment {
|
||||
val fragment = SearchResultsFragment()
|
||||
val arguments = Bundle()
|
||||
arguments.putString(ARG_SEARCHER, searchProvider.name)
|
||||
arguments.putString(ARG_QUERY, query)
|
||||
fragment.arguments = arguments
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ import ac.mdiq.podcini.ui.activity.MainActivity
|
|||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.Spinner
|
||||
import ac.mdiq.podcini.ui.dialog.FeedFilterDialog
|
||||
import ac.mdiq.podcini.ui.dialog.FeedSortDialog
|
||||
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
||||
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
|
||||
|
@ -32,6 +31,7 @@ import ac.mdiq.podcini.util.Logd
|
|||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
@ -69,6 +69,10 @@ import androidx.media3.common.util.UnstableApi
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
|
@ -77,6 +81,7 @@ import com.leinardi.android.speeddial.SpeedDialActionItem
|
|||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.text.NumberFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
@ -184,7 +189,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
|
||||
val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd
|
||||
subscriptionAddButton.setOnClickListener {
|
||||
if (activity is MainActivity) (activity as MainActivity).loadChildFragment(AddFeedFragment())
|
||||
if (activity is MainActivity) (activity as MainActivity).loadChildFragment(OnlineSearchFragment())
|
||||
}
|
||||
|
||||
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
|
@ -1075,6 +1080,128 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
}
|
||||
}
|
||||
|
||||
class FeedFilterDialog : BottomSheetDialogFragment() {
|
||||
private lateinit var rows: LinearLayout
|
||||
private var _binding: FilterDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
var filter: FeedFilter? = null
|
||||
private val buttonMap: MutableMap<String, Button> = mutableMapOf()
|
||||
|
||||
private val newFilterValues: Set<String>
|
||||
get() {
|
||||
val newFilterValues: MutableSet<String> = HashSet()
|
||||
for (i in 0 until rows.childCount) {
|
||||
if (rows.getChildAt(i) !is MaterialButtonToggleGroup) continue
|
||||
val group = rows.getChildAt(i) as MaterialButtonToggleGroup
|
||||
if (group.checkedButtonId == View.NO_ID) continue
|
||||
val tag = group.findViewById<View>(group.checkedButtonId).tag as? String ?: continue
|
||||
newFilterValues.add(tag)
|
||||
}
|
||||
return newFilterValues
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val layout = inflater.inflate(R.layout.filter_dialog, container, false)
|
||||
_binding = FilterDialogBinding.bind(layout)
|
||||
rows = binding.filterRows
|
||||
Logd("FeedFilterDialog", "fragment onCreateView")
|
||||
|
||||
//add filter rows
|
||||
for (item in FeedFilterGroup.entries) {
|
||||
// Logd("EpisodeFilterDialog", "FeedItemFilterGroup: ${item.values[0].filterId} ${item.values[1].filterId}")
|
||||
val rBinding = FilterDialogRowBinding.inflate(inflater)
|
||||
// rowBinding.root.addOnButtonCheckedListener { _: MaterialButtonToggleGroup?, _: Int, _: Boolean ->
|
||||
// onFilterChanged(newFilterValues)
|
||||
// }
|
||||
rBinding.filterButton1.setOnClickListener { onFilterChanged(newFilterValues) }
|
||||
rBinding.filterButton2.setOnClickListener { onFilterChanged(newFilterValues) }
|
||||
|
||||
rBinding.filterButton1.setText(item.values[0].displayName)
|
||||
rBinding.filterButton1.tag = item.values[0].filterId
|
||||
buttonMap[item.values[0].filterId] = rBinding.filterButton1
|
||||
rBinding.filterButton2.setText(item.values[1].displayName)
|
||||
rBinding.filterButton2.tag = item.values[1].filterId
|
||||
buttonMap[item.values[1].filterId] = rBinding.filterButton2
|
||||
rBinding.filterButton1.maxLines = 3
|
||||
rBinding.filterButton1.isSingleLine = false
|
||||
rBinding.filterButton2.maxLines = 3
|
||||
rBinding.filterButton2.isSingleLine = false
|
||||
rows.addView(rBinding.root, rows.childCount - 1)
|
||||
}
|
||||
|
||||
binding.confirmFiltermenu.setOnClickListener { dismiss() }
|
||||
binding.resetFiltermenu.setOnClickListener {
|
||||
onFilterChanged(emptySet())
|
||||
for (i in 0 until rows.childCount) {
|
||||
if (rows.getChildAt(i) is MaterialButtonToggleGroup) (rows.getChildAt(i) as MaterialButtonToggleGroup).clearChecked()
|
||||
}
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
for (filterId in filter!!.values) {
|
||||
if (filterId.isNotEmpty()) {
|
||||
val button = buttonMap[filterId]
|
||||
if (button != null) (button.parent as MaterialButtonToggleGroup).check(button.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.setOnShowListener { dialogInterface: DialogInterface ->
|
||||
val bottomSheetDialog = dialogInterface as BottomSheetDialog
|
||||
setupFullHeight(bottomSheetDialog)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupFullHeight(bottomSheetDialog: BottomSheetDialog) {
|
||||
val bottomSheet = bottomSheetDialog.findViewById<View>(com.leinardi.android.speeddial.R.id.design_bottom_sheet) as? FrameLayout
|
||||
if (bottomSheet != null) {
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
val layoutParams = bottomSheet.layoutParams
|
||||
bottomSheet.layoutParams = layoutParams
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
feedsFilter = StringUtils.join(newFilterValues, ",")
|
||||
Logd(TAG, "onFilterChanged: $feedsFilter")
|
||||
EventFlow.postEvent(FlowEvent.FeedsFilterEvent(newFilterValues))
|
||||
}
|
||||
|
||||
enum class FeedFilterGroup(vararg values: ItemProperties) {
|
||||
KEEP_UPDATED(ItemProperties(R.string.keep_updated, FeedFilter.States.keepUpdated.name), ItemProperties(R.string.not_keep_updated, FeedFilter.States.not_keepUpdated.name)),
|
||||
PLAY_SPEED(ItemProperties(R.string.global_speed, FeedFilter.States.global_playSpeed.name), ItemProperties(R.string.custom_speed, FeedFilter.States.custom_playSpeed.name)),
|
||||
SKIPS(ItemProperties(R.string.has_skips, FeedFilter.States.has_skips.name), ItemProperties(R.string.no_skips, FeedFilter.States.no_skips.name)),
|
||||
AUTO_DELETE(ItemProperties(R.string.always_auto_delete, FeedFilter.States.always_auto_delete.name), ItemProperties(R.string.never_auto_delete, FeedFilter.States.never_auto_delete.name)),
|
||||
AUTO_DOWNLOAD(ItemProperties(R.string.auto_download, FeedFilter.States.autoDownload.name), ItemProperties(R.string.not_auto_download, FeedFilter.States.not_autoDownload.name));
|
||||
|
||||
@JvmField
|
||||
val values: Array<ItemProperties> = arrayOf(*values)
|
||||
|
||||
class ItemProperties(@JvmField val displayName: Int, @JvmField val filterId: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(filter: FeedFilter?): FeedFilterDialog {
|
||||
val dialog = FeedFilterDialog()
|
||||
dialog.filter = filter
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
|
|
|
@ -62,10 +62,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
private val binding get() = _binding!!
|
||||
private lateinit var root: ViewGroup
|
||||
|
||||
/**
|
||||
* True if video controls are currently visible.
|
||||
*/
|
||||
private var videoControlsShowing = true
|
||||
private var videoControlsVisible = true
|
||||
private var videoSurfaceCreated = false
|
||||
private var lastScreenTap: Long = 0
|
||||
private val videoControlsHider = Handler(Looper.getMainLooper())
|
||||
|
@ -75,10 +72,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
private var itemsLoaded = false
|
||||
private var episode: Episode? = null
|
||||
private var webviewData: String? = null
|
||||
var webvDescription: ShownotesWebView? = null
|
||||
private var webvDescription: ShownotesWebView? = null
|
||||
|
||||
var destroyingDueToReload = false
|
||||
var controller: ServiceStatusHandler? = null
|
||||
var statusHandler: ServiceStatusHandler? = null
|
||||
var isFavorite = false
|
||||
|
||||
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
||||
|
@ -93,16 +90,15 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
onRewind()
|
||||
showSkipAnimation(false)
|
||||
}
|
||||
if (videoControlsShowing) {
|
||||
if (videoControlsVisible) {
|
||||
hideVideoControls(false)
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||
videoControlsShowing = false
|
||||
videoControlsVisible = false
|
||||
}
|
||||
return@OnTouchListener true
|
||||
}
|
||||
toggleVideoControlsVisibility()
|
||||
if (videoControlsShowing) setupVideoControlsToggler()
|
||||
|
||||
if (videoControlsVisible) setupVideoControlsToggler()
|
||||
lastScreenTap = System.currentTimeMillis()
|
||||
true
|
||||
}
|
||||
|
@ -111,7 +107,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
holder.setFixedSize(width, height)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Logd(TAG, "Videoview holder created")
|
||||
|
@ -120,7 +115,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
||||
setupVideoAspectRatio()
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Logd(TAG, "Videosurface was destroyed")
|
||||
videoSurfaceCreated = false
|
||||
|
@ -132,26 +126,28 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
|
||||
private val hideVideoControls = Runnable {
|
||||
if (videoControlsShowing) {
|
||||
if (videoControlsVisible) {
|
||||
Logd(TAG, "Hiding video controls")
|
||||
hideVideoControls(true)
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as? AppCompatActivity)?.supportActionBar?.hide()
|
||||
videoControlsShowing = false
|
||||
videoControlsVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
root = binding.root
|
||||
controller = newPlaybackController()
|
||||
controller!!.init()
|
||||
statusHandler = newStatusHandler()
|
||||
statusHandler!!.init()
|
||||
// loadMediaInfo()
|
||||
setupView()
|
||||
return root
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun newPlaybackController(): ServiceStatusHandler {
|
||||
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
|
||||
return object : ServiceStatusHandler(requireActivity()) {
|
||||
override fun updatePlayButton(showPlay: Boolean) {
|
||||
Logd(TAG, "updatePlayButtonShowsPlay called")
|
||||
|
@ -160,7 +156,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
else {
|
||||
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setupVideoAspectRatio()
|
||||
if (videoSurfaceCreated && controller != null) {
|
||||
if (videoSurfaceCreated) {
|
||||
Logd(TAG, "Videosurface already created, setting videosurface now")
|
||||
// setVideoSurface(binding.videoView.holder)
|
||||
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
||||
|
@ -208,8 +204,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
webvDescription!!.destroy()
|
||||
}
|
||||
_binding = null
|
||||
controller?.release()
|
||||
controller = null // prevent leak
|
||||
statusHandler?.release()
|
||||
statusHandler = null // prevent leak
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
@ -259,7 +255,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
|
||||
private fun setupVideoAspectRatio() {
|
||||
if (videoSurfaceCreated && controller != null) {
|
||||
if (videoSurfaceCreated) {
|
||||
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
|
||||
Logd(TAG, "Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
|
||||
val videoWidth = resources.displayMetrics.widthPixels
|
||||
|
@ -279,7 +275,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
|
||||
private var loadItemsRunning = false
|
||||
@OptIn(UnstableApi::class) private fun loadMediaInfo() {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun loadMediaInfo() {
|
||||
Logd(TAG, "loadMediaInfo called")
|
||||
if (curMedia == null) return
|
||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
||||
|
@ -314,14 +311,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
|
||||
"text/html", "utf-8", "about:blank")
|
||||
|
||||
itemsLoaded = true
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
} finally {
|
||||
loadItemsRunning = false
|
||||
}
|
||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||
} finally { loadItemsRunning = false }
|
||||
}
|
||||
}
|
||||
val media = curMedia
|
||||
|
@ -396,16 +389,14 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
|
||||
fun toggleVideoControlsVisibility() {
|
||||
if (videoControlsShowing) {
|
||||
if (videoControlsVisible) {
|
||||
hideVideoControls(true)
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) {
|
||||
(activity as AppCompatActivity).supportActionBar?.hide()
|
||||
}
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||
} else {
|
||||
showVideoControls()
|
||||
(activity as AppCompatActivity).supportActionBar?.show()
|
||||
}
|
||||
videoControlsShowing = !videoControlsShowing
|
||||
videoControlsVisible = !videoControlsVisible
|
||||
}
|
||||
|
||||
fun showSkipAnimation(isForward: Boolean) {
|
||||
|
@ -448,7 +439,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
|
||||
@UnstableApi
|
||||
fun onRewind() {
|
||||
if (controller == null) return
|
||||
// if (statusHandler == null) return
|
||||
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
@ -461,7 +452,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
|
||||
@UnstableApi
|
||||
fun onFastForward() {
|
||||
if (controller == null) return
|
||||
// if (statusHandler == null) return
|
||||
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
@ -503,7 +494,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
|
||||
private fun onPositionObserverUpdate() {
|
||||
if (controller == null) return
|
||||
// if (statusHandler == null) return
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
val currentPosition = converter.convert(curPositionFB)
|
||||
val duration_ = converter.convert(curDurationFB)
|
||||
|
@ -527,7 +518,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (controller == null) return
|
||||
// if (statusHandler == null) return
|
||||
if (fromUser) {
|
||||
prog = progress / (seekBar.max.toFloat())
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.view
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.actions.handler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.util.*
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ac.mdiq.podcini.ui.widget
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils.getFallbackImageLocation
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createPendingIntent
|
||||
|
@ -10,15 +9,17 @@ import ac.mdiq.podcini.receiver.PlayerWidget.Companion.isEnabled
|
|||
import ac.mdiq.podcini.receiver.PlayerWidget.Companion.prefs
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.activity.starter.PlaybackSpeedActivityStarter
|
||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils.getFallbackImageLocation
|
||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.util.Log
|
||||
|
@ -35,7 +36,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
/**
|
||||
* Updates the state of the player widget.
|
||||
*/
|
||||
|
@ -206,4 +206,30 @@ object WidgetUpdater {
|
|||
class WidgetState(val media: Playable?, val status: PlayerStatus, val position: Int, val duration: Int, val playbackSpeed: Float) {
|
||||
constructor(status: PlayerStatus) : this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the playback speed dialog activity of the app with specific arguments.
|
||||
* Does not require a dependency on the actual implementation of the activity.
|
||||
*/
|
||||
class PlaybackSpeedActivityStarter(private val context: Context) {
|
||||
val intent: Intent = Intent(INTENT)
|
||||
|
||||
init {
|
||||
intent.setPackage(context.packageName)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_playback_speed, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
fun start() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INTENT: String = "ac.mdiq.podcini.intents.PLAYBACK_SPEED"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -38,16 +38,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/searchButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:contentDescription="@string/search_podcast_hint"
|
||||
android:scaleType="center"
|
||||
app:srcCompat="@drawable/ic_search" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/combinedFeedSearchEditText"
|
||||
android:layout_width="0dp"
|
||||
|
@ -65,6 +55,16 @@
|
|||
android:hint="@string/search_podcast_hint"
|
||||
android:background="@null" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/searchButton"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:contentDescription="@string/search_podcast_hint"
|
||||
android:scaleType="center"
|
||||
app:srcCompat="@drawable/ic_search" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/fragment_online_search">
|
||||
android:id="@+id/fragment_search_results">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
|
@ -1,3 +1,11 @@
|
|||
# 6.5.4
|
||||
|
||||
* in the search bar of OnlineSearch view, search button is moved to the end of the bar
|
||||
* new way of handling sharing of youtube channels from other apps
|
||||
* normal text (other than url) shared from other apps is taken by OnlineSearch view for podcast search
|
||||
* preparing mediaSouces is done in IO scope preventing network access blocking Main scope
|
||||
* some class restructuring and refactoring
|
||||
|
||||
# 6.5.3
|
||||
|
||||
* properly assigning ids to remote episodes in OnlineFeedView to resolve the issue of duplicates
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Version 6.5.4 brings several changes:
|
||||
|
||||
* in the search bar of OnlineSearch view, search button is moved to the end of the bar
|
||||
* new way of handling sharing of youtube channels from other apps
|
||||
* normal text (other than url) shared from other apps is taken by OnlineSearch view for podcast search
|
||||
* preparing mediaSouces is done in IO scope preventing network access blocking Main scope
|
||||
* some class restructuring and refactoring
|
Loading…
Reference in New Issue