6.5.4 commit
This commit is contained in:
parent
943ead25bf
commit
782c582db6
|
@ -31,8 +31,8 @@ android {
|
||||||
testApplicationId "ac.mdiq.podcini.tests"
|
testApplicationId "ac.mdiq.podcini.tests"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
versionCode 3020237
|
versionCode 3020238
|
||||||
versionName "6.5.3"
|
versionName "6.5.4"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
|
|
@ -263,7 +263,7 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.activity.OnlineFeedViewActivity"
|
android:name=".ui.activity.ShareReceiverActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:theme="@style/Theme.Podcini.Dark.Translucent"
|
android:theme="@style/Theme.Podcini.Dark.Translucent"
|
||||||
android:label="@string/add_feed_label"
|
android:label="@string/add_feed_label"
|
||||||
|
|
|
@ -218,10 +218,10 @@ class FeedHandler {
|
||||||
state.namespaces[uri] = Itunes()
|
state.namespaces[uri] = Itunes()
|
||||||
Logd(TAG, "Recognized ITunes namespace")
|
Logd(TAG, "Recognized ITunes namespace")
|
||||||
}
|
}
|
||||||
uri == YouTube.NSURI && prefix == YouTube.NSTAG -> {
|
// uri == YouTube.NSURI && prefix == YouTube.NSTAG -> {
|
||||||
state.namespaces[uri] = YouTube()
|
// state.namespaces[uri] = YouTube()
|
||||||
Logd(TAG, "Recognized YouTube namespace")
|
// Logd(TAG, "Recognized YouTube namespace")
|
||||||
}
|
// }
|
||||||
uri == SimpleChapters.NSURI && prefix.matches(SimpleChapters.NSTAG.toRegex()) -> {
|
uri == SimpleChapters.NSURI && prefix.matches(SimpleChapters.NSTAG.toRegex()) -> {
|
||||||
state.namespaces[uri] = SimpleChapters()
|
state.namespaces[uri] = SimpleChapters()
|
||||||
Logd(TAG, "Recognized SimpleChapters namespace")
|
Logd(TAG, "Recognized SimpleChapters namespace")
|
||||||
|
@ -238,6 +238,7 @@ class FeedHandler {
|
||||||
state.namespaces[uri] = PodcastIndex()
|
state.namespaces[uri] = PodcastIndex()
|
||||||
Logd(TAG, "Recognized PodcastIndex namespace")
|
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 ac.mdiq.podcini.net.feed.parser.utils.DurationParser.inMillis
|
||||||
import org.xml.sax.Attributes
|
import org.xml.sax.Attributes
|
||||||
|
|
||||||
|
// TODO: this appears not needed
|
||||||
class YouTube : Namespace() {
|
class YouTube : Namespace() {
|
||||||
val TAG = this::class.simpleName ?: "Anonymous"
|
val TAG = this::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
|
|
|
@ -138,9 +138,7 @@ abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
|
||||||
try {
|
try {
|
||||||
activity.unregisterReceiver(statusUpdate)
|
activity.unregisterReceiver(statusUpdate)
|
||||||
activity.unregisterReceiver(notificationReceiver)
|
activity.unregisterReceiver(notificationReceiver)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {/* ignore */ }
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
initialized = false
|
initialized = false
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||||
private fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) {
|
private fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) {
|
||||||
|
Logd(TAG, "setDataSource1 called")
|
||||||
val url = media.getStreamUrl() ?: return
|
val url = media.getStreamUrl() ?: return
|
||||||
val preferences = media.episodeOrFetch()?.feed?.preferences
|
val preferences = media.episodeOrFetch()?.feed?.preferences
|
||||||
val user = preferences?.username
|
val user = preferences?.username
|
||||||
|
@ -191,7 +192,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
val vService = Vista.getService(0)
|
val vService = Vista.getService(0)
|
||||||
val streamInfo = StreamInfo.getInfo(vService, url)
|
val streamInfo = StreamInfo.getInfo(vService, url)
|
||||||
val audioStreamsList = getFilteredAudioStreams(streamInfo.audioStreams)
|
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 audioIndex = if (isNetworkRestricted) 0 else audioStreamsList.size - 1
|
||||||
val audioStream = audioStreamsList[audioIndex]
|
val audioStream = audioStreamsList[audioIndex]
|
||||||
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
|
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}")
|
||||||
|
@ -346,31 +347,37 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
callback.ensureMediaInfoLoaded(curMedia!!)
|
callback.ensureMediaInfoLoaded(curMedia!!)
|
||||||
callback.onMediaChanged(false)
|
callback.onMediaChanged(false)
|
||||||
setPlaybackParams(getCurrentPlaybackSpeed(curMedia), UserPreferences.isSkipSilence)
|
setPlaybackParams(getCurrentPlaybackSpeed(curMedia), UserPreferences.isSkipSilence)
|
||||||
when {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
streaming -> {
|
when {
|
||||||
val streamurl = curMedia!!.getStreamUrl()
|
streaming -> {
|
||||||
if (streamurl != null) {
|
val streamurl = curMedia!!.getStreamUrl()
|
||||||
val media = curMedia
|
if (streamurl != null) {
|
||||||
if (media is EpisodeMedia) {
|
val media = curMedia
|
||||||
val deferred = CoroutineScope(Dispatchers.IO).async { setDataSource(metadata, media) }
|
if (media is EpisodeMedia) {
|
||||||
if (startWhenPrepared) runBlocking { deferred.await() }
|
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
|
// val preferences = media.episodeOrFetch()?.feed?.preferences
|
||||||
// setDataSource(metadata, streamurl, preferences?.username, preferences?.password)
|
// setDataSource(metadata, streamurl, preferences?.username, preferences?.password)
|
||||||
} else setDataSource(metadata, streamurl, null, null)
|
} else setDataSource(metadata, streamurl, null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else -> {
|
||||||
else -> {
|
val localMediaurl = curMedia!!.getLocalMediaUrl()
|
||||||
val localMediaurl = curMedia!!.getLocalMediaUrl()
|
|
||||||
// File(localMediaurl).canRead() time consuming, leave it to MediaItem to handle
|
// 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() && File(localMediaurl).canRead()) setDataSource(metadata, localMediaurl, null, null)
|
||||||
if (!localMediaurl.isNullOrEmpty()) setDataSource(metadata, localMediaurl, null, null)
|
if (!localMediaurl.isNullOrEmpty()) setDataSource(metadata, localMediaurl, null, null)
|
||||||
else throw IOException("Unable to read local file $localMediaurl")
|
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) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
setPlayerStatus(PlayerStatus.ERROR, null)
|
setPlayerStatus(PlayerStatus.ERROR, null)
|
||||||
|
@ -394,6 +401,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
seekTo(newPosition)
|
seekTo(newPosition)
|
||||||
}
|
}
|
||||||
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR()
|
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR()
|
||||||
|
// while (mediaItem == null && mediaSource == null) runBlocking { delay(100) }
|
||||||
exoPlayer?.play()
|
exoPlayer?.play()
|
||||||
// Can't set params when paused - so always set it on start in case they changed
|
// Can't set params when paused - so always set it on start in case they changed
|
||||||
exoPlayer?.playbackParameters = playbackParameters
|
exoPlayer?.playbackParameters = playbackParameters
|
||||||
|
|
|
@ -101,7 +101,6 @@ import kotlin.math.max
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class PlaybackService : MediaLibraryService() {
|
class PlaybackService : MediaLibraryService() {
|
||||||
|
|
||||||
private var mediaSession: MediaLibrarySession? = null
|
private var mediaSession: MediaLibrarySession? = null
|
||||||
|
|
||||||
internal var mPlayer: MediaPlayerBase? = 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 {
|
private val mediaPlayerCallback: MediaPlayerCallback = object : MediaPlayerCallback {
|
||||||
override fun statusChanged(newInfo: MediaPlayerInfo?) {
|
override fun statusChanged(newInfo: MediaPlayerInfo?) {
|
||||||
currentMediaType = mPlayer?.mediaType ?: MediaType.UNKNOWN
|
currentMediaType = mPlayer?.mediaType ?: MediaType.UNKNOWN
|
||||||
|
@ -391,7 +421,6 @@ class PlaybackService : MediaLibraryService() {
|
||||||
j = if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) curIndexInQueue else eList.size-1
|
j = if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) curIndexInQueue else eList.size-1
|
||||||
} else if (i < eList.size-1) j = i+1
|
} else if (i < eList.size-1) j = i+1
|
||||||
Logd(TAG, "getNextInQueue next j: $j")
|
Logd(TAG, "getNextInQueue next j: $j")
|
||||||
|
|
||||||
val nextItem = unmanaged(eList[j])
|
val nextItem = unmanaged(eList[j])
|
||||||
Logd(TAG, "getNextInQueue nextItem ${nextItem.title}")
|
Logd(TAG, "getNextInQueue nextItem ${nextItem.title}")
|
||||||
if (nextItem.media == null) {
|
if (nextItem.media == null) {
|
||||||
|
@ -399,13 +428,11 @@ class PlaybackService : MediaLibraryService() {
|
||||||
writeNoMediaPlaying()
|
writeNoMediaPlaying()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFollowQueue) {
|
if (!isFollowQueue) {
|
||||||
Logd(TAG, "getNextInQueue(), but follow queue is not enabled.")
|
Logd(TAG, "getNextInQueue(), but follow queue is not enabled.")
|
||||||
writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED)
|
writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed?.isLocalFeed != true) {
|
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed?.isLocalFeed != true) {
|
||||||
Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}")
|
Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}")
|
||||||
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent)
|
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent)
|
||||||
|
@ -433,11 +460,9 @@ class PlaybackService : MediaLibraryService() {
|
||||||
else -> EXTRA_CODE_AUDIO
|
else -> EXTRA_CODE_AUDIO
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ensureMediaInfoLoaded(media: Playable) {
|
override fun ensureMediaInfoLoaded(media: Playable) {
|
||||||
// if (media is EpisodeMedia && media.item == null) media.item = DBReader.getFeedItem(media.itemId)
|
// if (media is EpisodeMedia && media.item == null) media.item = DBReader.getFeedItem(media.itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun writeMediaPlaying(playable: Playable?, playerStatus: PlayerStatus) {
|
fun writeMediaPlaying(playable: Playable?, playerStatus: PlayerStatus) {
|
||||||
Logd(InTheatre.TAG, "Writing playback preferences ${playable?.getIdentifier()}")
|
Logd(InTheatre.TAG, "Writing playback preferences ${playable?.getIdentifier()}")
|
||||||
if (playable == null) writeNoMediaPlaying()
|
if (playable == null) writeNoMediaPlaying()
|
||||||
|
@ -457,14 +482,12 @@ class PlaybackService : MediaLibraryService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun writePlayerStatus(playerStatus: PlayerStatus) {
|
fun writePlayerStatus(playerStatus: PlayerStatus) {
|
||||||
Logd(InTheatre.TAG, "Writing player status playback preferences")
|
Logd(InTheatre.TAG, "Writing player status playback preferences")
|
||||||
curState = upsertBlk(curState) {
|
curState = upsertBlk(curState) {
|
||||||
it.curPlayerStatus = getCurPlayerStatusAsInt(playerStatus)
|
it.curPlayerStatus = getCurPlayerStatusAsInt(playerStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCurPlayerStatusAsInt(playerStatus: PlayerStatus): Int {
|
private fun getCurPlayerStatusAsInt(playerStatus: PlayerStatus): Int {
|
||||||
val playerStatusAsInt = when (playerStatus) {
|
val playerStatusAsInt = when (playerStatus) {
|
||||||
PlayerStatus.PLAYING -> PLAYER_STATUS_PLAYING
|
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 {
|
private val mediaLibrarySessionCK = object: MediaLibrarySession.Callback {
|
||||||
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
||||||
Logd(TAG, "in MyMediaSessionCallback onConnect")
|
Logd(TAG, "in MyMediaSessionCallback onConnect")
|
||||||
|
@ -762,9 +754,9 @@ class PlaybackService : MediaLibraryService() {
|
||||||
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
|
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
|
||||||
val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
|
val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
|
||||||
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
|
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)
|
intent?.getParcelableExtra(EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||||
} else {
|
else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
intent?.getParcelableExtra(EXTRA_KEY_EVENT)
|
intent?.getParcelableExtra(EXTRA_KEY_EVENT)
|
||||||
}
|
}
|
||||||
|
@ -797,6 +789,7 @@ class PlaybackService : MediaLibraryService() {
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
playable != null -> {
|
playable != null -> {
|
||||||
|
recreateMediaSessionIfNeeded()
|
||||||
Logd(TAG, "onStartCommand status: $status")
|
Logd(TAG, "onStartCommand status: $status")
|
||||||
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
|
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
|
||||||
val allowStreamAlways = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_ALWAYS, false) ?: false
|
val allowStreamAlways = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_ALWAYS, false) ?: false
|
||||||
|
@ -963,6 +956,7 @@ class PlaybackService : MediaLibraryService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlayingFromPreferences() {
|
private fun startPlayingFromPreferences() {
|
||||||
|
recreateMediaSessionIfNeeded()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) { loadPlayableFromPreferences() }
|
withContext(Dispatchers.IO) { loadPlayableFromPreferences() }
|
||||||
|
@ -975,7 +969,7 @@ class PlaybackService : MediaLibraryService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlaying(allowStreamThisTime: Boolean) {
|
private fun startPlaying(allowStreamThisTime: Boolean) {
|
||||||
Logd(TAG, "startPlaying called $allowStreamThisTime")
|
Logd(TAG, "startPlaying called allowStreamThisTime: $allowStreamThisTime")
|
||||||
val media = curMedia ?: return
|
val media = curMedia ?: return
|
||||||
|
|
||||||
val localFeed = URLUtil.isContentUrl(media.getStreamUrl())
|
val localFeed = URLUtil.isContentUrl(media.getStreamUrl())
|
||||||
|
@ -986,18 +980,17 @@ class PlaybackService : MediaLibraryService() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.getIdentifier() != curState.curMediaId) clearCurTempSpeed()
|
// TODO: this is redundant
|
||||||
|
// if (media.getIdentifier() != curState.curMediaId) clearCurTempSpeed()
|
||||||
|
|
||||||
mPlayer?.playMediaObject(media, streaming, startWhenPrepared = true, true)
|
mPlayer?.playMediaObject(media, streaming, startWhenPrepared = true, true)
|
||||||
recreateMediaSessionIfNeeded()
|
// recreateMediaSessionIfNeeded()
|
||||||
// val episode = (media as? EpisodeMedia)?.episode
|
// val episode = (media as? EpisodeMedia)?.episode
|
||||||
// if (curMedia is EpisodeMedia && episode != null) addToQueue(true, episode)
|
// if (curMedia is EpisodeMedia && episode != null) addToQueue(true, episode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCurTempSpeed() {
|
fun clearCurTempSpeed() {
|
||||||
curState = upsertBlk(curState) {
|
curState = upsertBlk(curState) { it.curTempSpeed = FeedPreferences.SPEED_USE_GLOBAL }
|
||||||
it.curTempSpeed = FeedPreferences.SPEED_USE_GLOBAL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var eventSink: Job? = null
|
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.R
|
||||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
|
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.R
|
||||||
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
|
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.Menu
|
||||||
import android.view.MenuItem
|
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
|
package ac.mdiq.podcini.ui.actions.swipeactions
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
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.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.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.dialog.SwipeActionsDialog
|
||||||
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
|
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
|
||||||
import ac.mdiq.podcini.ui.fragment.DownloadsFragment
|
import ac.mdiq.podcini.ui.fragment.DownloadsFragment
|
||||||
import ac.mdiq.podcini.ui.fragment.HistoryFragment
|
import ac.mdiq.podcini.ui.fragment.HistoryFragment
|
||||||
import ac.mdiq.podcini.ui.fragment.QueuesFragment
|
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.utils.ThemeUtils.getColorFromAttr
|
||||||
import ac.mdiq.podcini.ui.view.EpisodeViewHolder
|
import ac.mdiq.podcini.ui.view.EpisodeViewHolder
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.os.Handler
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -23,7 +44,13 @@ import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
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.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.sin
|
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)
|
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
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
|
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.feed.discovery.ItunesTopListLoader
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
|
@ -417,7 +418,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment()
|
AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment()
|
||||||
DownloadsFragment.TAG -> fragment = DownloadsFragment()
|
DownloadsFragment.TAG -> fragment = DownloadsFragment()
|
||||||
HistoryFragment.TAG -> fragment = HistoryFragment()
|
HistoryFragment.TAG -> fragment = HistoryFragment()
|
||||||
AddFeedFragment.TAG -> fragment = AddFeedFragment()
|
OnlineSearchFragment.TAG -> fragment = OnlineSearchFragment()
|
||||||
SubscriptionsFragment.TAG -> fragment = SubscriptionsFragment()
|
SubscriptionsFragment.TAG -> fragment = SubscriptionsFragment()
|
||||||
StatisticsFragment.TAG -> fragment = StatisticsFragment()
|
StatisticsFragment.TAG -> fragment = StatisticsFragment()
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -622,11 +623,11 @@ class MainActivity : CastEnabledActivity() {
|
||||||
when {
|
when {
|
||||||
intent.hasExtra(Extras.fragment_feed_id.name) -> {
|
intent.hasExtra(Extras.fragment_feed_id.name) -> {
|
||||||
val feedId = intent.getLongExtra(Extras.fragment_feed_id.name, 0)
|
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) {
|
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)
|
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)
|
else loadFeedFragmentById(feedId, args)
|
||||||
}
|
}
|
||||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||||
|
@ -635,25 +636,26 @@ class MainActivity : CastEnabledActivity() {
|
||||||
val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name)
|
val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name)
|
||||||
if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl))
|
if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl))
|
||||||
}
|
}
|
||||||
intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG) -> {
|
intent.hasExtra(Extras.search_string.name) -> {
|
||||||
val tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)
|
val query = intent.getStringExtra(Extras.search_string.name)
|
||||||
val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS)
|
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)
|
if (tag != null) loadFragment(tag, args)
|
||||||
|
|
||||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
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
|
// bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
// bottomSheetCallback.onSlide(dummyView, 1.0f)
|
// bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||||
}
|
}
|
||||||
else -> handleDeeplink(intent.data)
|
else -> handleDeeplink(intent.data)
|
||||||
}
|
}
|
||||||
|
if (intent.getBooleanExtra(MainActivityStarter.Extras.open_drawer.name, false)) drawerLayout?.open()
|
||||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false)) drawerLayout?.open()
|
if (intent.getBooleanExtra(MainActivityStarter.Extras.open_download_logs.name, false))
|
||||||
|
|
||||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false))
|
|
||||||
DownloadLogFragment().show(supportFragmentManager, null)
|
DownloadLogFragment().show(supportFragmentManager, null)
|
||||||
|
|
||||||
if (intent.getBooleanExtra(Extras.refresh_on_start.name, false)) runOnceOrAsk(this)
|
if (intent.getBooleanExtra(Extras.refresh_on_start.name, false)) runOnceOrAsk(this)
|
||||||
|
|
||||||
// to avoid handling the intent twice when the configuration changes
|
// to avoid handling the intent twice when the configuration changes
|
||||||
|
@ -756,9 +758,10 @@ class MainActivity : CastEnabledActivity() {
|
||||||
fragment_feed_id,
|
fragment_feed_id,
|
||||||
fragment_feed_url,
|
fragment_feed_url,
|
||||||
refresh_on_start,
|
refresh_on_start,
|
||||||
started_from_search,
|
started_from_share, // TODO: seems not needed
|
||||||
add_to_back_stack,
|
add_to_back_stack,
|
||||||
generated_view_id,
|
generated_view_id,
|
||||||
|
search_string,
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -781,5 +784,13 @@ class MainActivity : CastEnabledActivity() {
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
return intent
|
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
// this now is only used for receiving shared feed url
|
class ShareReceiverActivity : AppCompatActivity() {
|
||||||
class OnlineFeedViewActivity : AppCompatActivity() {
|
|
||||||
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
|
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -31,21 +31,29 @@ class OnlineFeedViewActivity : AppCompatActivity() {
|
||||||
if (urlString != null) feedUrl = URLDecoder.decode(urlString, "UTF-8")
|
if (urlString != null) feedUrl = URLDecoder.decode(urlString, "UTF-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feedUrl == null) {
|
when {
|
||||||
Log.e(TAG, "feedUrl is null.")
|
feedUrl.isNullOrBlank() -> {
|
||||||
showNoPodcastFoundError()
|
Log.e(TAG, "feedUrl is empty or null.")
|
||||||
} else {
|
showNoPodcastFoundError()
|
||||||
Logd(TAG, "Activity was started with url $feedUrl")
|
}
|
||||||
val intent = MainActivity.showOnlineFeed(this, feedUrl)
|
!feedUrl.matches(Regex("[./%]")) -> {
|
||||||
intent.putExtra(MainActivity.Extras.started_from_search.name, getIntent().getBooleanExtra(MainActivity.Extras.started_from_search.name, false))
|
val intent = MainActivity.showOnlineSearch(this, feedUrl)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
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() {
|
private fun showNoPodcastFoundError() {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
MaterialAlertDialogBuilder(this@OnlineFeedViewActivity)
|
MaterialAlertDialogBuilder(this@ShareReceiverActivity)
|
||||||
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> finish() }
|
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> finish() }
|
||||||
.setTitle(R.string.error_label)
|
.setTitle(R.string.error_label)
|
||||||
.setMessage(R.string.null_value_podcast_error)
|
.setMessage(R.string.null_value_podcast_error)
|
||||||
|
@ -64,6 +72,6 @@ class OnlineFeedViewActivity : AppCompatActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
const val ARG_FEEDURL: String = "arg.feedurl"
|
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||||
private const val RESULT_ERROR = 2
|
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
|
videoEpisodeFragment.isFavorite = false
|
||||||
invalidateOptionsMenu()
|
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")
|
SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
|
||||||
}
|
|
||||||
item.itemId == R.id.audio_controls -> {
|
item.itemId == R.id.audio_controls -> {
|
||||||
val dialog = PlaybackControlsDialog.newInstance()
|
val dialog = PlaybackControlsDialog.newInstance()
|
||||||
dialog.show(supportFragmentManager, "playback_controls")
|
dialog.show(supportFragmentManager, "playback_controls")
|
||||||
|
@ -374,24 +373,22 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
private lateinit var dialog: AlertDialog
|
private lateinit var dialog: AlertDialog
|
||||||
private var _binding: AudioControlsBinding? = null
|
private var _binding: AudioControlsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private var controller: ServiceStatusHandler? = null
|
private var statusHandler: ServiceStatusHandler? = null
|
||||||
|
|
||||||
@UnstableApi override fun onStart() {
|
@UnstableApi override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
controller = object : ServiceStatusHandler(requireActivity()) {
|
statusHandler = object : ServiceStatusHandler(requireActivity()) {
|
||||||
override fun loadMediaInfo() {
|
override fun loadMediaInfo() {
|
||||||
setupAudioTracks()
|
setupAudioTracks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
controller?.init()
|
statusHandler?.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi override fun onStop() {
|
@UnstableApi override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
controller?.release()
|
statusHandler?.release()
|
||||||
controller = null
|
statusHandler = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
_binding = AudioControlsBinding.inflate(layoutInflater)
|
_binding = AudioControlsBinding.inflate(layoutInflater)
|
||||||
dialog = MaterialAlertDialogBuilder(requireContext())
|
dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
@ -400,20 +397,17 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
.setPositiveButton(R.string.close_label, null).create()
|
.setPositiveButton(R.string.close_label, null).create()
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
Logd(TAG, "onDestroyView")
|
Logd(TAG, "onDestroyView")
|
||||||
_binding = null
|
_binding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun setupAudioTracks() {
|
@UnstableApi private fun setupAudioTracks() {
|
||||||
val butAudioTracks = binding.audioTracks
|
val butAudioTracks = binding.audioTracks
|
||||||
if (audioTracks.size < 2 || selectedAudioTrack < 0) {
|
if (audioTracks.size < 2 || selectedAudioTrack < 0) {
|
||||||
butAudioTracks.visibility = View.GONE
|
butAudioTracks.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
butAudioTracks.visibility = View.VISIBLE
|
butAudioTracks.visibility = View.VISIBLE
|
||||||
butAudioTracks.text = audioTracks[selectedAudioTrack]
|
butAudioTracks.text = audioTracks[selectedAudioTrack]
|
||||||
butAudioTracks.setOnClickListener {
|
butAudioTracks.setOnClickListener {
|
||||||
|
|
|
@ -20,7 +20,7 @@ class MainActivityStarter(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIntent(): Intent {
|
fun getIntent(): Intent {
|
||||||
if (fragmentArgs != null) intent.putExtra(EXTRA_FRAGMENT_ARGS, fragmentArgs)
|
if (fragmentArgs != null) intent.putExtra(Extras.fragment_args.name, fragmentArgs)
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,32 +33,32 @@ class MainActivityStarter(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withOpenPlayer(): MainActivityStarter {
|
fun withOpenPlayer(): MainActivityStarter {
|
||||||
intent.putExtra(EXTRA_OPEN_PLAYER, true)
|
intent.putExtra(Extras.open_player.name, true)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withOpenFeed(feedId: Long): MainActivityStarter {
|
fun withOpenFeed(feedId: Long): MainActivityStarter {
|
||||||
intent.putExtra(EXTRA_FEED_ID, feedId)
|
intent.putExtra(Extras.fragment_feed_id.name, feedId)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withAddToBackStack(): MainActivityStarter {
|
fun withAddToBackStack(): MainActivityStarter {
|
||||||
intent.putExtra(EXTRA_ADD_TO_BACK_STACK, true)
|
intent.putExtra(Extras.add_to_back_stack.name, true)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withFragmentLoaded(fragmentName: String?): MainActivityStarter {
|
fun withFragmentLoaded(fragmentName: String?): MainActivityStarter {
|
||||||
intent.putExtra(EXTRA_FRAGMENT_TAG, fragmentName)
|
intent.putExtra(Extras.fragment_tag.name, fragmentName)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withDrawerOpen(): MainActivityStarter {
|
fun withDrawerOpen(): MainActivityStarter {
|
||||||
intent.putExtra(EXTRA_OPEN_DRAWER, true)
|
intent.putExtra(Extras.open_drawer.name, true)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withDownloadLogsOpen(): MainActivityStarter {
|
fun withDownloadLogsOpen(): MainActivityStarter {
|
||||||
intent.putExtra(EXTRA_OPEN_DOWNLOAD_LOGS, true)
|
intent.putExtra(Extras.open_download_logs.name, true)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +69,17 @@ class MainActivityStarter(private val context: Context) {
|
||||||
return this
|
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 {
|
companion object {
|
||||||
const val INTENT: String = "ac.mdiq.podcini.intents.MAIN_ACTIVITY"
|
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.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.ui.actions.actionbutton.*
|
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.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment
|
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment
|
||||||
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
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.annotation.OptIn
|
||||||
import androidx.media3.common.util.UnstableApi
|
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 lateinit var keys: List<SwipeAction>
|
||||||
|
|
||||||
private var rightAction: SwipeAction? = null
|
private var rightAction: SwipeAction? = null
|
||||||
|
|
|
@ -45,7 +45,8 @@ import java.text.DecimalFormat
|
||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
import java.util.*
|
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 adapter: SpeedSelectionAdapter
|
||||||
private lateinit var speedSeekBar: PlaybackSpeedSeekBar
|
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.ChapterUtils
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
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.MainActivity
|
||||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.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.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
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.actions.swipeactions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
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.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
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.actionbutton.DeleteActionButton
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
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.model.EpisodeSortOrder.Companion.fromCode
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
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.actions.swipeactions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||||
|
|
|
@ -116,7 +116,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnvRelatedFeeds.setOnClickListener {
|
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)
|
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||||
}
|
}
|
||||||
binding.txtvUrl.setOnClickListener(copyUrlToClipboard)
|
binding.txtvUrl.setOnClickListener(copyUrlToClipboard)
|
||||||
|
|
|
@ -186,7 +186,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
||||||
HistoryFragment.TAG -> R.drawable.ic_history
|
HistoryFragment.TAG -> R.drawable.ic_history
|
||||||
SubscriptionsFragment.TAG -> R.drawable.ic_subscriptions
|
SubscriptionsFragment.TAG -> R.drawable.ic_subscriptions
|
||||||
StatisticsFragment.TAG -> R.drawable.ic_chart_box
|
StatisticsFragment.TAG -> R.drawable.ic_chart_box
|
||||||
AddFeedFragment.TAG -> R.drawable.ic_add
|
OnlineSearchFragment.TAG -> R.drawable.ic_add
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,7 +384,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
||||||
DownloadsFragment.TAG,
|
DownloadsFragment.TAG,
|
||||||
HistoryFragment.TAG,
|
HistoryFragment.TAG,
|
||||||
StatisticsFragment.TAG,
|
StatisticsFragment.TAG,
|
||||||
AddFeedFragment.TAG,
|
OnlineSearchFragment.TAG,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun saveLastNavFragment(tag: String?) {
|
fun saveLastNavFragment(tag: String?) {
|
||||||
|
|
|
@ -94,6 +94,7 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
|
|
||||||
var feedSource: String = ""
|
var feedSource: String = ""
|
||||||
var feedUrl: String = ""
|
var feedUrl: String = ""
|
||||||
|
|
||||||
private val feedId: Long
|
private val feedId: Long
|
||||||
get() {
|
get() {
|
||||||
if (feeds == null) return 0
|
if (feeds == null) return 0
|
||||||
|
@ -128,6 +129,7 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||||
|
|
||||||
feedUrl = requireArguments().getString(ARG_FEEDURL) ?: ""
|
feedUrl = requireArguments().getString(ARG_FEEDURL) ?: ""
|
||||||
|
Logd(TAG, "feedUrl: $feedUrl")
|
||||||
if (feedUrl.isEmpty()) {
|
if (feedUrl.isEmpty()) {
|
||||||
Log.e(TAG, "feedUrl is null.")
|
Log.e(TAG, "feedUrl is null.")
|
||||||
showNoPodcastFoundError()
|
showNoPodcastFoundError()
|
||||||
|
@ -194,8 +196,7 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
private fun lookupUrlAndBuild(url: String) {
|
private fun lookupUrlAndBuild(url: String) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
||||||
try {
|
try { startFeedBuilding(urlString)
|
||||||
startFeedBuilding(urlString)
|
|
||||||
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
@ -258,7 +259,8 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
|
|
||||||
private fun startFeedBuilding(url: String) {
|
private fun startFeedBuilding(url: String) {
|
||||||
Logd(TAG, "startFeedBuilding")
|
Logd(TAG, "startFeedBuilding")
|
||||||
if (feedSource == "VistaGuide") {
|
if (feedSource == "VistaGuide" || url.contains("youtube.com")) {
|
||||||
|
feedSource = "VistaGuide"
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
feeds = getFeedList()
|
feeds = getFeedList()
|
||||||
|
|
|
@ -1,198 +1,210 @@
|
||||||
package ac.mdiq.podcini.ui.fragment
|
package ac.mdiq.podcini.ui.fragment
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.FragmentOnlineSearchBinding
|
import ac.mdiq.podcini.databinding.AddfeedBinding
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcher
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
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.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 ac.mdiq.podcini.util.Logd
|
||||||
import android.content.Context
|
import android.content.*
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.*
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides actions for adding new podcast subscriptions.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
class OnlineSearchFragment : Fragment() {
|
class OnlineSearchFragment : Fragment() {
|
||||||
|
|
||||||
private var _binding: FragmentOnlineSearchBinding? = null
|
private var _binding: AddfeedBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private var adapter: OnlineFeedsAdapter? = null
|
private var activity: MainActivity? = null
|
||||||
private var searchProvider: PodcastSearcher? = null
|
private var displayUpArrow = false
|
||||||
|
|
||||||
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 val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||||
* List of podcasts retreived from the search
|
this.chooseOpmlImportPathResult(uri) }
|
||||||
*/
|
|
||||||
private var searchResults: MutableList<PodcastSearchResult>? = null
|
|
||||||
// private var disposable: Disposable? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) { uri: Uri? -> this.addLocalFolderResult(uri) }
|
||||||
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)
|
|
||||||
|
|
||||||
|
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")
|
Logd(TAG, "fragment onCreateView")
|
||||||
gridView = binding.gridView
|
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||||
adapter = OnlineFeedsAdapter(requireContext(), ArrayList())
|
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||||
gridView.setAdapter(adapter)
|
(getActivity() as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||||
|
|
||||||
//Show information about the podcast when the list item is clicked
|
binding.searchButton.setOnClickListener { performSearch() }
|
||||||
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
binding.searchVistaGuideButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
|
||||||
val podcast = searchResults!![position]
|
binding.searchItunesButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }
|
||||||
if (podcast.feedUrl != null) {
|
binding.searchFyydButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }
|
||||||
val fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
binding.searchGPodderButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
|
||||||
fragment.feedSource = podcast.source
|
binding.searchPodcastIndexButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
|
||||||
(activity as MainActivity).loadChildFragment(fragment)
|
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
|
binding.addLocalFolderButton.setOnClickListener {
|
||||||
txtvError = binding.txtvError
|
try { addLocalFolderLauncher.launch(null)
|
||||||
butRetry = binding.butRetry
|
} catch (e: ActivityNotFoundException) {
|
||||||
txtvEmpty = binding.empty
|
e.printStackTrace()
|
||||||
if (searchProvider != null) binding.searchPoweredBy.text = getString(R.string.search_powered_by, searchProvider!!.name)
|
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||||
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) {}
|
}
|
||||||
})
|
|
||||||
|
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
|
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
|
_binding = null
|
||||||
searchResults = null
|
super.onDestroyView()
|
||||||
adapter = null
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupToolbar(toolbar: MaterialToolbar) {
|
private fun chooseOpmlImportPathResult(uri: Uri?) {
|
||||||
toolbar.inflateMenu(R.menu.online_search)
|
if (uri == null) return
|
||||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
|
||||||
|
|
||||||
val searchItem: MenuItem = toolbar.menu.findItem(R.id.action_search)
|
val intent = Intent(context, OpmlImportActivity::class.java)
|
||||||
val sv = searchItem.actionView as? SearchView
|
intent.setData(uri)
|
||||||
if (sv != null) {
|
startActivity(intent)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun search(query: String) {
|
@UnstableApi private fun addLocalFolderResult(uri: Uri?) {
|
||||||
showOnlyProgressBar()
|
if (uri == null) return
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val result = searchProvider?.search(query)
|
val feed = withContext(Dispatchers.IO) { addLocalFolder(uri) }
|
||||||
searchResults = result?.toMutableList()
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
progressBar.visibility = View.GONE
|
if (feed != null) {
|
||||||
adapter?.clear()
|
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||||
handleSearchResults()
|
(getActivity() as MainActivity).loadChildFragment(fragment)
|
||||||
txtvEmpty.text = getString(R.string.no_results_for_query, query)
|
}
|
||||||
}
|
}
|
||||||
} 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() {
|
@UnstableApi private fun addLocalFolder(uri: Uri): Feed? {
|
||||||
adapter?.addAll(searchResults!!)
|
requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
adapter?.notifyDataSetInvalidated()
|
val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||||
gridView.visibility = if (!searchResults.isNullOrEmpty()) View.VISIBLE else View.GONE
|
requireNotNull(documentFile) { "Unable to retrieve document tree" }
|
||||||
txtvEmpty.visibility = if (searchResults.isNullOrEmpty()) View.VISIBLE else View.GONE
|
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) {
|
private class AddLocalFolder : ActivityResultContracts.OpenDocumentTree() {
|
||||||
Logd(TAG, "exception: ${e.message}")
|
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||||
progressBar.visibility = View.GONE
|
return super.createIntent(context, input).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = OnlineSearchFragment::class.simpleName ?: "Anonymous"
|
val TAG = OnlineSearchFragment::class.simpleName ?: "Anonymous"
|
||||||
private const val ARG_SEARCHER = "searcher"
|
private const val KEY_UP_ARROW = "up_arrow"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import ac.mdiq.podcini.storage.model.PlayQueue
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
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.actions.swipeactions.SwipeActions
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
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.BuildConfig
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
|
import ac.mdiq.podcini.databinding.FragmentSearchResultsBinding
|
||||||
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryBinding
|
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryBinding
|
||||||
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryItemBinding
|
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
|
||||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader.Companion.prefs
|
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader.Companion.prefs
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
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.Logd
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import coil.load
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
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 {
|
companion object {
|
||||||
private val TAG: String = QuickDiscoveryFragment::class.simpleName ?: "Anonymous"
|
private val TAG: String = QuickDiscoveryFragment::class.simpleName ?: "Anonymous"
|
||||||
private const val NUM_SUGGESTIONS = 12
|
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.Episode
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
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.menuhandler.EpisodeMenuHandler
|
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||||
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.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
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.Logd
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
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.
|
* 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 var _binding: SearchFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
@ -290,15 +292,14 @@ import java.lang.ref.WeakReference
|
||||||
search()
|
search()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatMatches")
|
||||||
@UnstableApi private fun search() {
|
@UnstableApi private fun search() {
|
||||||
adapterFeeds.setEndButton(R.string.search_online) { this.searchOnline() }
|
adapterFeeds.setEndButton(R.string.search_online) { this.searchOnline() }
|
||||||
chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE
|
chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val results = withContext(Dispatchers.IO) {
|
val results = withContext(Dispatchers.IO) { performSearch() }
|
||||||
performSearch()
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
progressBar.visibility = View.GONE
|
progressBar.visibility = View.GONE
|
||||||
if (results.first != null) {
|
if (results.first != null) {
|
||||||
|
@ -398,7 +399,7 @@ import java.lang.ref.WeakReference
|
||||||
(activity as MainActivity).loadChildFragment(fragment)
|
(activity as MainActivity).loadChildFragment(fragment)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
(activity as MainActivity).loadChildFragment(OnlineSearchFragment.newInstance(CombinedSearcher::class.java, query))
|
(activity as MainActivity).loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartSelectMode() {
|
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.adapter.SelectableAdapter
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||||
import ac.mdiq.podcini.ui.compose.Spinner
|
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.FeedSortDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
|
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.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
@ -69,6 +69,10 @@ import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
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.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.elevation.SurfaceColors
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
@ -77,6 +81,7 @@ import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||||
import com.leinardi.android.speeddial.SpeedDialView
|
import com.leinardi.android.speeddial.SpeedDialView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -184,7 +189,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
|
|
||||||
val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd
|
val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd
|
||||||
subscriptionAddButton.setOnClickListener {
|
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()
|
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 {
|
companion object {
|
||||||
val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous"
|
val TAG = SubscriptionsFragment::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
|
|
|
@ -62,10 +62,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private lateinit var root: ViewGroup
|
private lateinit var root: ViewGroup
|
||||||
|
|
||||||
/**
|
private var videoControlsVisible = true
|
||||||
* True if video controls are currently visible.
|
|
||||||
*/
|
|
||||||
private var videoControlsShowing = true
|
|
||||||
private var videoSurfaceCreated = false
|
private var videoSurfaceCreated = false
|
||||||
private var lastScreenTap: Long = 0
|
private var lastScreenTap: Long = 0
|
||||||
private val videoControlsHider = Handler(Looper.getMainLooper())
|
private val videoControlsHider = Handler(Looper.getMainLooper())
|
||||||
|
@ -75,10 +72,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
private var itemsLoaded = false
|
private var itemsLoaded = false
|
||||||
private var episode: Episode? = null
|
private var episode: Episode? = null
|
||||||
private var webviewData: String? = null
|
private var webviewData: String? = null
|
||||||
var webvDescription: ShownotesWebView? = null
|
private var webvDescription: ShownotesWebView? = null
|
||||||
|
|
||||||
var destroyingDueToReload = false
|
var destroyingDueToReload = false
|
||||||
var controller: ServiceStatusHandler? = null
|
var statusHandler: ServiceStatusHandler? = null
|
||||||
var isFavorite = false
|
var isFavorite = false
|
||||||
|
|
||||||
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
||||||
|
@ -93,16 +90,15 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
onRewind()
|
onRewind()
|
||||||
showSkipAnimation(false)
|
showSkipAnimation(false)
|
||||||
}
|
}
|
||||||
if (videoControlsShowing) {
|
if (videoControlsVisible) {
|
||||||
hideVideoControls(false)
|
hideVideoControls(false)
|
||||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||||
videoControlsShowing = false
|
videoControlsVisible = false
|
||||||
}
|
}
|
||||||
return@OnTouchListener true
|
return@OnTouchListener true
|
||||||
}
|
}
|
||||||
toggleVideoControlsVisibility()
|
toggleVideoControlsVisibility()
|
||||||
if (videoControlsShowing) setupVideoControlsToggler()
|
if (videoControlsVisible) setupVideoControlsToggler()
|
||||||
|
|
||||||
lastScreenTap = System.currentTimeMillis()
|
lastScreenTap = System.currentTimeMillis()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -111,7 +107,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||||
holder.setFixedSize(width, height)
|
holder.setFixedSize(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
Logd(TAG, "Videoview holder created")
|
Logd(TAG, "Videoview holder created")
|
||||||
|
@ -120,7 +115,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
||||||
setupVideoAspectRatio()
|
setupVideoAspectRatio()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
Logd(TAG, "Videosurface was destroyed")
|
Logd(TAG, "Videosurface was destroyed")
|
||||||
videoSurfaceCreated = false
|
videoSurfaceCreated = false
|
||||||
|
@ -132,26 +126,28 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val hideVideoControls = Runnable {
|
private val hideVideoControls = Runnable {
|
||||||
if (videoControlsShowing) {
|
if (videoControlsVisible) {
|
||||||
Logd(TAG, "Hiding video controls")
|
Logd(TAG, "Hiding video controls")
|
||||||
hideVideoControls(true)
|
hideVideoControls(true)
|
||||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as? AppCompatActivity)?.supportActionBar?.hide()
|
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)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
Logd(TAG, "fragment onCreateView")
|
||||||
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
root = binding.root
|
root = binding.root
|
||||||
controller = newPlaybackController()
|
statusHandler = newStatusHandler()
|
||||||
controller!!.init()
|
statusHandler!!.init()
|
||||||
// loadMediaInfo()
|
// loadMediaInfo()
|
||||||
setupView()
|
setupView()
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) private fun newPlaybackController(): ServiceStatusHandler {
|
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
|
||||||
return object : ServiceStatusHandler(requireActivity()) {
|
return object : ServiceStatusHandler(requireActivity()) {
|
||||||
override fun updatePlayButton(showPlay: Boolean) {
|
override fun updatePlayButton(showPlay: Boolean) {
|
||||||
Logd(TAG, "updatePlayButtonShowsPlay called")
|
Logd(TAG, "updatePlayButtonShowsPlay called")
|
||||||
|
@ -160,7 +156,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
else {
|
else {
|
||||||
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
setupVideoAspectRatio()
|
setupVideoAspectRatio()
|
||||||
if (videoSurfaceCreated && controller != null) {
|
if (videoSurfaceCreated) {
|
||||||
Logd(TAG, "Videosurface already created, setting videosurface now")
|
Logd(TAG, "Videosurface already created, setting videosurface now")
|
||||||
// setVideoSurface(binding.videoView.holder)
|
// setVideoSurface(binding.videoView.holder)
|
||||||
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
||||||
|
@ -208,8 +204,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
webvDescription!!.destroy()
|
webvDescription!!.destroy()
|
||||||
}
|
}
|
||||||
_binding = null
|
_binding = null
|
||||||
controller?.release()
|
statusHandler?.release()
|
||||||
controller = null // prevent leak
|
statusHandler = null // prevent leak
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +255,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupVideoAspectRatio() {
|
private fun setupVideoAspectRatio() {
|
||||||
if (videoSurfaceCreated && controller != null) {
|
if (videoSurfaceCreated) {
|
||||||
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
|
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
|
||||||
Logd(TAG, "Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
|
Logd(TAG, "Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
|
||||||
val videoWidth = resources.displayMetrics.widthPixels
|
val videoWidth = resources.displayMetrics.widthPixels
|
||||||
|
@ -279,7 +275,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadItemsRunning = false
|
private var loadItemsRunning = false
|
||||||
@OptIn(UnstableApi::class) private fun loadMediaInfo() {
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun loadMediaInfo() {
|
||||||
Logd(TAG, "loadMediaInfo called")
|
Logd(TAG, "loadMediaInfo called")
|
||||||
if (curMedia == null) return
|
if (curMedia == null) return
|
||||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
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!!,
|
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
|
||||||
"text/html", "utf-8", "about:blank")
|
"text/html", "utf-8", "about:blank")
|
||||||
|
|
||||||
itemsLoaded = true
|
itemsLoaded = true
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
} finally { loadItemsRunning = false }
|
||||||
} finally {
|
|
||||||
loadItemsRunning = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val media = curMedia
|
val media = curMedia
|
||||||
|
@ -396,16 +389,14 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleVideoControlsVisibility() {
|
fun toggleVideoControlsVisibility() {
|
||||||
if (videoControlsShowing) {
|
if (videoControlsVisible) {
|
||||||
hideVideoControls(true)
|
hideVideoControls(true)
|
||||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) {
|
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||||
(activity as AppCompatActivity).supportActionBar?.hide()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showVideoControls()
|
showVideoControls()
|
||||||
(activity as AppCompatActivity).supportActionBar?.show()
|
(activity as AppCompatActivity).supportActionBar?.show()
|
||||||
}
|
}
|
||||||
videoControlsShowing = !videoControlsShowing
|
videoControlsVisible = !videoControlsVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSkipAnimation(isForward: Boolean) {
|
fun showSkipAnimation(isForward: Boolean) {
|
||||||
|
@ -448,7 +439,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onRewind() {
|
fun onRewind() {
|
||||||
if (controller == null) return
|
// if (statusHandler == null) return
|
||||||
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||||
setupVideoControlsToggler()
|
setupVideoControlsToggler()
|
||||||
}
|
}
|
||||||
|
@ -461,7 +452,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onFastForward() {
|
fun onFastForward() {
|
||||||
if (controller == null) return
|
// if (statusHandler == null) return
|
||||||
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||||
setupVideoControlsToggler()
|
setupVideoControlsToggler()
|
||||||
}
|
}
|
||||||
|
@ -503,7 +494,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPositionObserverUpdate() {
|
private fun onPositionObserverUpdate() {
|
||||||
if (controller == null) return
|
// if (statusHandler == null) return
|
||||||
val converter = TimeSpeedConverter(curSpeedFB)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
val currentPosition = converter.convert(curPositionFB)
|
val currentPosition = converter.convert(curPositionFB)
|
||||||
val duration_ = converter.convert(curDurationFB)
|
val duration_ = converter.convert(curDurationFB)
|
||||||
|
@ -527,7 +518,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
if (controller == null) return
|
// if (statusHandler == null) return
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
prog = progress / (seekBar.max.toFloat())
|
prog = progress / (seekBar.max.toFloat())
|
||||||
val converter = TimeSpeedConverter(curSpeedFB)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.view
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils
|
import ac.mdiq.podcini.net.utils.NetworkUtils
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
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.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||||
import ac.mdiq.podcini.util.*
|
import ac.mdiq.podcini.util.*
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ac.mdiq.podcini.ui.widget
|
package ac.mdiq.podcini.ui.widget
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils.getFallbackImageLocation
|
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
||||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createPendingIntent
|
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.receiver.PlayerWidget.Companion.prefs
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
import ac.mdiq.podcini.storage.model.MediaType
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
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.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.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.appwidget.AppWidgetManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
@ -35,7 +36,6 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the state of the player widget.
|
* 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) {
|
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)
|
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:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
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
|
<EditText
|
||||||
android:id="@+id/combinedFeedSearchEditText"
|
android:id="@+id/combinedFeedSearchEditText"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -65,6 +55,16 @@
|
||||||
android:hint="@string/search_podcast_hint"
|
android:hint="@string/search_podcast_hint"
|
||||||
android:background="@null" />
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appbar"
|
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
|
# 6.5.3
|
||||||
|
|
||||||
* properly assigning ids to remote episodes in OnlineFeedView to resolve the issue of duplicates
|
* 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