mirror of
https://github.com/XilinJia/Podcini.git
synced 2025-01-31 14:14:59 +01:00
6.8.3 commit
This commit is contained in:
parent
16841905dd
commit
89143c60d3
@ -31,8 +31,8 @@ android {
|
||||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020257
|
||||
versionName "6.8.0"
|
||||
versionCode 3020259
|
||||
versionName "6.8.2"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
@ -1,9 +1,13 @@
|
||||
package ac.mdiq.podcini.net.download
|
||||
|
||||
class DownloadStatus(@JvmField val state: Int, @JvmField val progress: Int) {
|
||||
companion object {
|
||||
const val STATE_QUEUED: Int = 0
|
||||
const val STATE_COMPLETED: Int = 1 // Both successful and not successful
|
||||
const val STATE_RUNNING: Int = 2
|
||||
class DownloadStatus(
|
||||
@JvmField val state: Int,
|
||||
@JvmField val progress: Int) {
|
||||
|
||||
enum class State {
|
||||
UNKNOWN,
|
||||
QUEUED,
|
||||
RUNNING,
|
||||
COMPLETED // Both successful and not successful
|
||||
}
|
||||
}
|
||||
|
@ -27,11 +27,11 @@ abstract class DownloadServiceInterface {
|
||||
abstract fun cancelAll(context: Context)
|
||||
|
||||
fun isDownloadingEpisode(url: String): Boolean {
|
||||
return (currentDownloads.containsKey(url) && currentDownloads[url]!!.state != DownloadStatus.STATE_COMPLETED)
|
||||
return (currentDownloads.containsKey(url) && currentDownloads[url]!!.state != DownloadStatus.State.COMPLETED.ordinal)
|
||||
}
|
||||
|
||||
fun isEpisodeQueued(url: String): Boolean {
|
||||
return (currentDownloads.containsKey(url) && currentDownloads[url]!!.state == DownloadStatus.STATE_QUEUED)
|
||||
return (currentDownloads.containsKey(url) && currentDownloads[url]!!.state == DownloadStatus.State.QUEUED.ordinal)
|
||||
}
|
||||
|
||||
fun getProgress(url: String): Int {
|
||||
|
@ -57,7 +57,7 @@ import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||
import ac.mdiq.podcini.ui.fragment.AudioPlayerFragment.PlayerDetailsFragment.Companion.media3Controller
|
||||
import ac.mdiq.podcini.ui.fragment.AudioPlayerFragment.Companion.media3Controller
|
||||
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
||||
import ac.mdiq.podcini.ui.widget.WidgetUpdater
|
||||
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
|
||||
@ -1708,7 +1708,7 @@ class PlaybackService : MediaLibraryService() {
|
||||
|
||||
when (status) {
|
||||
PlayerStatus.PLAYING, PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
||||
Logd(TAG, "seekTo() called $t")
|
||||
Logd(TAG, "seekTo t: $t")
|
||||
if (seekLatch != null && seekLatch!!.count > 0) {
|
||||
try { seekLatch!!.await(3, TimeUnit.SECONDS) } catch (e: InterruptedException) { Log.e(TAG, Log.getStackTraceString(e)) }
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.stream.StreamInfo
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import io.realm.kotlin.ext.realmListOf
|
||||
import io.realm.kotlin.ext.realmSetOf
|
||||
import io.realm.kotlin.types.RealmList
|
||||
@ -116,7 +120,7 @@ class Episode : RealmObject {
|
||||
val imageLocation: String?
|
||||
get() = when {
|
||||
imageUrl != null -> imageUrl
|
||||
media != null && unmanaged(media!!).hasEmbeddedPicture() -> EpisodeMedia.FILENAME_PREFIX_EMBEDDED_COVER + media!!.getLocalMediaUrl()
|
||||
media != null && media?.hasEmbeddedPicture() == true -> EpisodeMedia.FILENAME_PREFIX_EMBEDDED_COVER + media!!.getLocalMediaUrl()
|
||||
feed != null -> {
|
||||
feed!!.imageUrl
|
||||
}
|
||||
@ -133,6 +137,18 @@ class Episode : RealmObject {
|
||||
return field
|
||||
}
|
||||
|
||||
@Ignore
|
||||
val inQueueState = mutableStateOf(curQueue.contains(this))
|
||||
|
||||
@Ignore
|
||||
val isPlayingState = mutableStateOf(isCurrentlyPlaying(media))
|
||||
|
||||
@Ignore
|
||||
val downloadState = mutableIntStateOf(if (media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
|
||||
|
||||
@Ignore
|
||||
val stopMonitoring = mutableStateOf(false)
|
||||
|
||||
constructor() {
|
||||
this.playState = PlayState.UNPLAYED.code
|
||||
}
|
||||
@ -151,6 +167,13 @@ class Episode : RealmObject {
|
||||
this.feed = feed
|
||||
}
|
||||
|
||||
fun copyStates(other: Episode) {
|
||||
inQueueState.value = other.inQueueState.value
|
||||
isPlayingState.value = other.isPlayingState.value
|
||||
downloadState.value = other.downloadState.value
|
||||
stopMonitoring.value = other.stopMonitoring.value
|
||||
}
|
||||
|
||||
fun updateFromOther(other: Episode) {
|
||||
if (other.imageUrl != null) this.imageUrl = other.imageUrl
|
||||
if (other.title != null) title = other.title
|
||||
@ -256,12 +279,11 @@ class Episode : RealmObject {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Episode) return false
|
||||
return id == other.id && playState == other.playState
|
||||
return id == other.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = (id xor (id ushr 32)).toInt()
|
||||
result = 31 * result + playState.hashCode()
|
||||
val result = (id xor (id ushr 32)).toInt()
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.showStackTrace
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
@ -208,7 +209,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
||||
|
||||
fun hasEmbeddedPicture(): Boolean {
|
||||
// TODO: checkEmbeddedPicture needs to update current copy
|
||||
if (hasEmbeddedPicture == null) checkEmbeddedPicture()
|
||||
if (hasEmbeddedPicture == null) unmanaged(this).checkEmbeddedPicture()
|
||||
return hasEmbeddedPicture ?: false
|
||||
}
|
||||
|
||||
@ -303,7 +304,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
||||
override fun getImageLocation(): String? {
|
||||
return when {
|
||||
episode != null -> episode!!.imageLocation
|
||||
unmanaged(this).hasEmbeddedPicture() -> FILENAME_PREFIX_EMBEDDED_COVER + getLocalMediaUrl()
|
||||
hasEmbeddedPicture() -> FILENAME_PREFIX_EMBEDDED_COVER + getLocalMediaUrl()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ class CancelDownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
val item_ = upsertBlk(item) {
|
||||
it.disableAutoDownload()
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item_)) }
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item_))
|
||||
}
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
@ -22,5 +22,6 @@ class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
deleteEpisodesWarnLocal(context, listOf(item))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
builder.show()
|
||||
}
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
private fun shouldNotDownload(media: EpisodeMedia?): Boolean {
|
||||
|
@ -35,28 +35,30 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
|
||||
|
||||
var processing: Float = -1f
|
||||
|
||||
val actionState = mutableIntStateOf(0)
|
||||
|
||||
abstract fun getLabel(): Int
|
||||
|
||||
abstract fun getDrawable(): Int
|
||||
|
||||
abstract fun onClick(context: Context)
|
||||
|
||||
fun configure(button: View, icon: ImageView, context: Context) {
|
||||
button.visibility = visibility
|
||||
button.contentDescription = context.getString(getLabel())
|
||||
button.setOnClickListener { onClick(context) }
|
||||
button.setOnLongClickListener {
|
||||
val composeView = ComposeView(context).apply {
|
||||
setContent {
|
||||
val showDialog = remember { mutableStateOf(true) }
|
||||
CustomTheme(context) { AltActionsDialog(context, showDialog.value, onDismiss = { showDialog.value = false }) }
|
||||
}
|
||||
}
|
||||
(button as? ViewGroup)?.addView(composeView)
|
||||
true
|
||||
}
|
||||
icon.setImageResource(getDrawable())
|
||||
}
|
||||
// fun configure(button: View, icon: ImageView, context: Context) {
|
||||
// button.visibility = visibility
|
||||
// button.contentDescription = context.getString(getLabel())
|
||||
// button.setOnClickListener { onClick(context) }
|
||||
// button.setOnLongClickListener {
|
||||
// val composeView = ComposeView(context).apply {
|
||||
// setContent {
|
||||
// val showDialog = remember { mutableStateOf(true) }
|
||||
// CustomTheme(context) { AltActionsDialog(context, showDialog.value, onDismiss = { showDialog.value = false }) }
|
||||
// }
|
||||
// }
|
||||
// (button as? ViewGroup)?.addView(composeView)
|
||||
// true
|
||||
// }
|
||||
// icon.setImageResource(getDrawable())
|
||||
// }
|
||||
|
||||
@Composable
|
||||
fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
|
@ -21,6 +21,7 @@ class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
if (!item.isPlayed()) setPlayState(Episode.PlayState.PLAYED.code, true, item)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,5 +22,6 @@ class PauseActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
if (isCurrentlyPlaying(media)) context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE))
|
||||
// EventFlow.postEvent(FlowEvent.PlayEvent(item, Action.END))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
notifyMissingEpisodeMediaFile(context, media)
|
||||
return
|
||||
}
|
||||
|
||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
playbackService?.mPlayer?.resume()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
@ -47,8 +46,8 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
|
||||
playVideoIfNeeded(context, media)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,7 +29,6 @@ class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
Toast.makeText(context, R.string.no_media_label, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
playbackService?.mPlayer?.resume()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
@ -38,7 +37,7 @@ class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
|
||||
if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
return
|
||||
}
|
||||
stream(context, media)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
|
||||
class StreamingConfirmationDialog(private val context: Context, private val playable: Playable) {
|
||||
|
@ -157,6 +157,7 @@ class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
item.setPlayed(false)
|
||||
processing = 1f
|
||||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,5 +20,6 @@ class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
override fun onClick(context: Context) {
|
||||
if (!item.link.isNullOrEmpty()) openInBrowser(context, item.link!!)
|
||||
actionState.value = getLabel()
|
||||
}
|
||||
}
|
||||
|
@ -22,53 +22,41 @@ import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction.Companion.NO_ACTION
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
|
||||
import ac.mdiq.podcini.ui.fragment.DownloadsFragment
|
||||
import ac.mdiq.podcini.ui.fragment.HistoryFragment
|
||||
import ac.mdiq.podcini.ui.fragment.QueuesFragment
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.ui.view.EpisodeViewHolder
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Canvas
|
||||
import android.os.Handler
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.*
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.annimon.stream.Stream
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sin
|
||||
|
||||
open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private val tag: String)
|
||||
: ItemTouchHelper.SimpleCallback(dragDirs, ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT), DefaultLifecycleObserver {
|
||||
open class SwipeActions(private val fragment: Fragment, private val tag: String) : DefaultLifecycleObserver {
|
||||
|
||||
@set:JvmName("setFilterProperty")
|
||||
var filter: EpisodeFilter? = null
|
||||
|
||||
var actions: Actions? = null
|
||||
var swipeOutEnabled: Boolean = true
|
||||
var swipedOutTo: Int = 0
|
||||
private val itemTouchHelper = ItemTouchHelper(this)
|
||||
|
||||
init {
|
||||
actions = getPrefs(tag)
|
||||
}
|
||||
|
||||
constructor(fragment: Fragment, tag: String) : this(0, fragment, tag)
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
actions = getPrefs(tag)
|
||||
}
|
||||
@ -82,32 +70,6 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
||||
this.filter = filter
|
||||
}
|
||||
|
||||
fun attachTo(recyclerView: RecyclerView?): SwipeActions {
|
||||
itemTouchHelper.attachToRecyclerView(recyclerView)
|
||||
return this
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
itemTouchHelper.attachToRecyclerView(null)
|
||||
}
|
||||
|
||||
private val isSwipeActionEnabled: Boolean
|
||||
get() = isSwipeActionEnabled(tag)
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@UnstableApi override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
|
||||
if (actions != null && !actions!!.hasActions(swipeDir)) {
|
||||
showDialog()
|
||||
return
|
||||
}
|
||||
val item = (viewHolder as EpisodeViewHolder).episode
|
||||
if (actions != null && item != null && filter != null)
|
||||
(if (swipeDir == ItemTouchHelper.RIGHT) actions!!.right else actions!!.left)?.performAction(item, fragment, filter!!)
|
||||
}
|
||||
|
||||
fun showDialog() {
|
||||
SwipeActionsDialog(fragment.requireContext(), tag).show(object : SwipeActionsDialog.Callback {
|
||||
override fun onCall() {
|
||||
@ -117,93 +79,6 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
||||
})
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
|
||||
dx: Float, dy: Float, actionState: Int, isCurrentlyActive: Boolean) {
|
||||
var dx = dx
|
||||
val right: SwipeAction
|
||||
val left: SwipeAction
|
||||
if (actions != null && actions!!.hasActions()) {
|
||||
right = actions!!.right!!
|
||||
left = actions!!.left!!
|
||||
} else {
|
||||
left = ShowFirstSwipeDialogAction()
|
||||
right = left
|
||||
}
|
||||
|
||||
//check if it will be removed
|
||||
val item = (viewHolder as EpisodeViewHolder).episode
|
||||
var wontLeave = false
|
||||
if (item != null && filter != null) {
|
||||
val rightWillRemove = right.willRemove(filter!!, item)
|
||||
val leftWillRemove = left.willRemove(filter!!, item)
|
||||
wontLeave = (dx > 0 && !rightWillRemove) || (dx < 0 && !leftWillRemove)
|
||||
}
|
||||
//Limit swipe if it's not removed
|
||||
val maxMovement = recyclerView.width * 2 / 5
|
||||
val sign = (if (dx > 0) 1 else -1).toFloat()
|
||||
val limitMovement = min(maxMovement.toDouble(), (sign * dx).toDouble()).toFloat()
|
||||
val displacementPercentage = limitMovement / maxMovement
|
||||
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && wontLeave) {
|
||||
swipeOutEnabled = false
|
||||
val swipeThresholdReached = displacementPercentage == 1f
|
||||
|
||||
// Move slower when getting near the maxMovement
|
||||
dx = sign * maxMovement * sin((Math.PI / 2) * displacementPercentage).toFloat()
|
||||
|
||||
if (isCurrentlyActive) {
|
||||
val dir = if (dx > 0) ItemTouchHelper.RIGHT else ItemTouchHelper.LEFT
|
||||
swipedOutTo = if (swipeThresholdReached) dir else 0
|
||||
}
|
||||
} else swipeOutEnabled = true
|
||||
|
||||
//add color and icon
|
||||
val context = fragment.requireContext()
|
||||
val themeColor = getColorFromAttr(context, android.R.attr.colorBackground)
|
||||
val actionColor = getColorFromAttr(context, if (dx > 0) right.getActionColor() else left.getActionColor())
|
||||
val builder = RecyclerViewSwipeDecorator.Builder(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive)
|
||||
.addSwipeRightActionIcon(right.getActionIcon())
|
||||
.addSwipeLeftActionIcon(left.getActionIcon())
|
||||
.addSwipeRightBackgroundColor(getColorFromAttr(context, R.attr.background_elevated))
|
||||
.addSwipeLeftBackgroundColor(getColorFromAttr(context, R.attr.background_elevated))
|
||||
.setActionIconTint(ColorUtils.blendARGB(themeColor, actionColor, max(0.5, displacementPercentage.toDouble()).toFloat()))
|
||||
builder.create().decorate()
|
||||
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive)
|
||||
}
|
||||
|
||||
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
|
||||
return if (swipeOutEnabled) defaultValue * 1.5f else Float.MAX_VALUE
|
||||
}
|
||||
|
||||
override fun getSwipeVelocityThreshold(defaultValue: Float): Float {
|
||||
return if (swipeOutEnabled) defaultValue * 0.6f else 0f
|
||||
}
|
||||
|
||||
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
|
||||
return if (swipeOutEnabled) 0.6f else 1.0f
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
|
||||
if (swipedOutTo != 0) {
|
||||
onSwiped(viewHolder, swipedOutTo)
|
||||
swipedOutTo = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
return if (!isSwipeActionEnabled) makeMovementFlags(getDragDirs(recyclerView, viewHolder), 0)
|
||||
else super.getMovementFlags(recyclerView, viewHolder)
|
||||
}
|
||||
|
||||
fun startDrag(holder: EpisodeViewHolder?) {
|
||||
if (holder != null) itemTouchHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
class Actions(prefs: String?) {
|
||||
@JvmField
|
||||
var right: SwipeAction? = null
|
||||
@ -259,11 +134,11 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
||||
@OptIn(UnstableApi::class) @JvmStatic
|
||||
fun getPrefsWithDefaults(tag: String): Actions {
|
||||
val defaultActions = when (tag) {
|
||||
QueuesFragment.TAG -> NO_ACTION + "," + NO_ACTION
|
||||
DownloadsFragment.TAG -> NO_ACTION + "," + NO_ACTION
|
||||
HistoryFragment.TAG -> NO_ACTION + "," + NO_ACTION
|
||||
AllEpisodesFragment.TAG -> NO_ACTION + "," + NO_ACTION
|
||||
else -> NO_ACTION + "," + NO_ACTION
|
||||
QueuesFragment.TAG -> "$NO_ACTION,$NO_ACTION"
|
||||
DownloadsFragment.TAG -> "$NO_ACTION,$NO_ACTION"
|
||||
HistoryFragment.TAG -> "$NO_ACTION,$NO_ACTION"
|
||||
AllEpisodesFragment.TAG -> "$NO_ACTION,$NO_ACTION"
|
||||
else -> "$NO_ACTION,$NO_ACTION"
|
||||
}
|
||||
return getPrefs(tag, defaultActions)
|
||||
}
|
||||
@ -358,7 +233,7 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
||||
|
||||
class NoActionSwipeAction : SwipeAction {
|
||||
override fun getId(): String {
|
||||
return SwipeAction.NO_ACTION
|
||||
return NO_ACTION
|
||||
}
|
||||
|
||||
override fun getActionIcon(): Int {
|
||||
|
@ -27,7 +27,7 @@ import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
import ac.mdiq.podcini.ui.fragment.AudioPlayerFragment.PlayerDetailsFragment.Companion.media3Controller
|
||||
import ac.mdiq.podcini.ui.fragment.AudioPlayerFragment.Companion.media3Controller
|
||||
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
|
||||
import ac.mdiq.podcini.ui.utils.LockableBottomSheetBehavior
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getDrawableFromAttr
|
||||
@ -58,6 +58,7 @@ import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.EditText
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
@ -182,7 +183,7 @@ class MainActivity : CastEnabledActivity() {
|
||||
buildTags()
|
||||
monitorFeeds()
|
||||
// InTheatre.apply { }
|
||||
AudioPlayerFragment.PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
||||
AudioPlayerFragment.getSharedPrefs(this@MainActivity)
|
||||
PlayerWidget.getSharedPrefs(this@MainActivity)
|
||||
StatisticsFragment.getSharedPrefs(this@MainActivity)
|
||||
OnlineFeedFragment.getSharedPrefs(this@MainActivity)
|
||||
@ -283,21 +284,21 @@ class MainActivity : CastEnabledActivity() {
|
||||
// Logd(TAG, "workInfo.state: ${workInfo.state}")
|
||||
var status: Int
|
||||
status = when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> DownloadStatus.STATE_RUNNING
|
||||
WorkInfo.State.ENQUEUED, WorkInfo.State.BLOCKED -> DownloadStatus.STATE_QUEUED
|
||||
WorkInfo.State.SUCCEEDED -> DownloadStatus.STATE_COMPLETED
|
||||
WorkInfo.State.RUNNING -> DownloadStatus.State.RUNNING.ordinal
|
||||
WorkInfo.State.ENQUEUED, WorkInfo.State.BLOCKED -> DownloadStatus.State.QUEUED.ordinal
|
||||
WorkInfo.State.SUCCEEDED -> DownloadStatus.State.COMPLETED.ordinal
|
||||
WorkInfo.State.FAILED -> {
|
||||
Log.e(TAG, "download failed $downloadUrl")
|
||||
DownloadStatus.STATE_COMPLETED
|
||||
DownloadStatus.State.COMPLETED.ordinal
|
||||
}
|
||||
WorkInfo.State.CANCELLED -> {
|
||||
Logd(TAG, "download cancelled $downloadUrl")
|
||||
DownloadStatus.STATE_COMPLETED
|
||||
DownloadStatus.State.COMPLETED.ordinal
|
||||
}
|
||||
}
|
||||
var progress = workInfo.progress.getInt(DownloadServiceInterface.WORK_DATA_PROGRESS, -1)
|
||||
if (progress == -1 && status != DownloadStatus.STATE_COMPLETED) {
|
||||
status = DownloadStatus.STATE_QUEUED
|
||||
if (progress == -1 && status != DownloadStatus.State.COMPLETED.ordinal) {
|
||||
status = DownloadStatus.State.QUEUED.ordinal
|
||||
progress = 0
|
||||
}
|
||||
updatedEpisodes[downloadUrl] = DownloadStatus(status, progress)
|
||||
@ -398,7 +399,8 @@ class MainActivity : CastEnabledActivity() {
|
||||
params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right,
|
||||
navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0))
|
||||
mainView.layoutParams = params
|
||||
val playerView = findViewById<FragmentContainerView>(R.id.playerFragment1)
|
||||
// val playerView = findViewById<FragmentContainerView>(R.id.playerFragment1)
|
||||
val playerView = findViewById<ComposeView>(R.id.composeView1)
|
||||
val playerParams = playerView?.layoutParams as? MarginLayoutParams
|
||||
playerParams?.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
|
||||
playerView?.layoutParams = playerParams
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
||||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
@ -17,7 +17,7 @@ import ac.mdiq.podcini.ui.actions.actionbutton.EpisodeActionButton
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler.PutToQueueDialog
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment
|
||||
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
|
||||
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
@ -67,102 +67,122 @@ fun InforBar(text: MutableState<String>, leftAction: MutableState<SwipeAction?>,
|
||||
val textColor = MaterialTheme.colors.onSurface
|
||||
Logd("InforBar", "textState: ${text.value}")
|
||||
Row {
|
||||
Image(painter = painterResource(leftAction.value?.getActionIcon() ?:R.drawable.ic_questionmark), contentDescription = "left_action_icon",
|
||||
Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig))
|
||||
Image(painter = painterResource(R.drawable.baseline_arrow_left_alt_24), contentDescription = "left_arrow", Modifier.width(24.dp).height(24.dp))
|
||||
Icon(painter = painterResource(leftAction.value?.getActionIcon() ?:R.drawable.ic_questionmark), tint = textColor, contentDescription = "left_action_icon",
|
||||
modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig))
|
||||
Icon(painter = painterResource(R.drawable.baseline_arrow_left_alt_24), tint = textColor, contentDescription = "left_arrow", modifier = Modifier.width(24.dp).height(24.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text.value, color = textColor, style = MaterialTheme.typography.body2)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(painter = painterResource(R.drawable.baseline_arrow_right_alt_24), contentDescription = "right_arrow", Modifier.width(24.dp).height(24.dp))
|
||||
Image(painter = painterResource(rightAction.value?.getActionIcon() ?:R.drawable.ic_questionmark), contentDescription = "right_action_icon",
|
||||
Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig))
|
||||
Icon(painter = painterResource(R.drawable.baseline_arrow_right_alt_24), tint = textColor, contentDescription = "right_arrow", modifier = Modifier.width(24.dp).height(24.dp))
|
||||
Icon(painter = painterResource(rightAction.value?.getActionIcon() ?:R.drawable.ic_questionmark), tint = textColor, contentDescription = "right_action_icon",
|
||||
modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpisodeSpeedDial(activity: MainActivity, selected: List<Episode>, modifier: Modifier = Modifier) {
|
||||
val TAG = "EpisodeSpeedDial"
|
||||
fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList<Episode>, modifier: Modifier = Modifier) {
|
||||
val TAG = "EpisodeSpeedDial ${selected.size}"
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
val options = listOf<@Composable () -> Unit>(
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_delete: ${selected.size}")
|
||||
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "")
|
||||
Text(stringResource(id = R.string.delete_episode_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_download: ${selected.size}")
|
||||
for (episode in selected) {
|
||||
if (episode.media != null && episode.feed != null && !episode.feed!!.isLocalFeed) DownloadServiceInterface.get()?.download(activity, episode)
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "")
|
||||
Text(stringResource(id = R.string.download_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_mark_played: ${selected.size}")
|
||||
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "")
|
||||
Text(stringResource(id = R.string.toggle_played_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_playlist_remove: ${selected.size}")
|
||||
removeFromQueue(*selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "")
|
||||
Text(stringResource(id = R.string.remove_from_queue_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||
Queues.addToQueue(true, *selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||
Text(stringResource(id = R.string.add_to_queue_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||
PutToQueueDialog(activity, selected).show()
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||
Text(stringResource(id = R.string.put_in_queue_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_star: ${selected.size}")
|
||||
for (item in selected) {
|
||||
Episodes.setFavorite(item, null)
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "")
|
||||
Text(stringResource(id = R.string.toggle_favorite_label))
|
||||
} },
|
||||
{
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_delete: ${selected.size}")
|
||||
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "")
|
||||
Text(stringResource(id = R.string.delete_episode_label))
|
||||
}
|
||||
},
|
||||
{
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_download: ${selected.size}")
|
||||
for (episode in selected) {
|
||||
if (episode.media != null && episode.feed != null && !episode.feed!!.isLocalFeed) DownloadServiceInterface.get()
|
||||
?.download(activity, episode)
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "")
|
||||
Text(stringResource(id = R.string.download_label))
|
||||
}
|
||||
},
|
||||
{
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_mark_played: ${selected.size}")
|
||||
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "")
|
||||
Text(stringResource(id = R.string.toggle_played_label))
|
||||
}
|
||||
},
|
||||
{
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_playlist_remove: ${selected.size}")
|
||||
removeFromQueue(*selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "")
|
||||
Text(stringResource(id = R.string.remove_from_queue_label))
|
||||
}
|
||||
},
|
||||
{
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||
Queues.addToQueue(true, *selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||
Text(stringResource(id = R.string.add_to_queue_label))
|
||||
}
|
||||
},
|
||||
{
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||
PutToQueueDialog(activity, selected).show()
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||
Text(stringResource(id = R.string.put_in_queue_label))
|
||||
}
|
||||
},
|
||||
{
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
Logd(TAG, "ic_star: ${selected.size}")
|
||||
for (item in selected) {
|
||||
Episodes.setFavorite(item, null)
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "")
|
||||
Text(stringResource(id = R.string.toggle_favorite_label))
|
||||
}
|
||||
},
|
||||
)
|
||||
Column(modifier = modifier, verticalArrangement = Arrangement.Bottom) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.Bottom) {
|
||||
if (isExpanded) options.forEachIndexed { _, button ->
|
||||
FloatingActionButton(modifier = Modifier.padding(start = 4.dp, bottom = 6.dp).height(50.dp), backgroundColor = Color.LightGray, onClick = {}) { button() } }
|
||||
FloatingActionButton(onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") }
|
||||
FloatingActionButton(modifier = Modifier.padding(start = 4.dp, bottom = 6.dp).height(50.dp),
|
||||
backgroundColor = Color.LightGray,
|
||||
onClick = {}) { button() }
|
||||
}
|
||||
FloatingActionButton(backgroundColor = Color.Green,
|
||||
onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") }
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,10 +191,12 @@ fun EpisodeSpeedDial(activity: MainActivity, selected: List<Episode>, modifier:
|
||||
fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episode>, leftSwipeCB: (Episode) -> Unit, rightSwipeCB: (Episode) -> Unit, actionButton_: ((Episode)->EpisodeActionButton)? = null) {
|
||||
val TAG = "EpisodeLazyColumn"
|
||||
var selectMode by remember { mutableStateOf(false) }
|
||||
val selectedIds by remember { mutableStateOf(mutableSetOf<Long>()) }
|
||||
val selected = remember { mutableListOf<Episode>()}
|
||||
// val selectedIds = remember { mutableSetOf<Long>() }
|
||||
var selectedSize by remember { mutableStateOf(0) }
|
||||
val selected = remember { mutableStateListOf<Episode>() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val lazyListState = rememberLazyListState()
|
||||
var longPressIndex by remember { mutableIntStateOf(-1) }
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(state = lazyListState,
|
||||
@ -196,11 +218,12 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
when (changes) {
|
||||
is UpdatedObject -> {
|
||||
Logd(TAG, "episodeMonitor UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||
Logd(TAG, "episodeMonitor playedState0: $playedState ${episode.isPlayed()}")
|
||||
playedState = changes.obj.isPlayed()
|
||||
farvoriteState = changes.obj.isFavorite
|
||||
episodes[index] = changes.obj
|
||||
Logd(TAG, "episodeMonitor playedState: $playedState")
|
||||
// episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
||||
changes.obj.copyStates(episodes[index])
|
||||
episodes.removeAt(index)
|
||||
episodes.add(index, changes.obj)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
@ -217,7 +240,10 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
Logd(TAG, "mediaMonitor UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||
positionState = changes.obj.media?.position?:0
|
||||
inProgressState = changes.obj.isInProgress
|
||||
episodes[index] = changes.obj
|
||||
// episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
||||
changes.obj.copyStates(episodes[index])
|
||||
episodes.removeAt(index)
|
||||
episodes.add(index, changes.obj)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
@ -230,6 +256,10 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
mediaMonitor?.cancel()
|
||||
}
|
||||
}
|
||||
if (episodes[index].stopMonitoring.value) {
|
||||
episodeMonitor?.cancel()
|
||||
mediaMonitor?.cancel()
|
||||
}
|
||||
val velocityTracker = remember { VelocityTracker() }
|
||||
val offsetX = remember { Animatable(0f) }
|
||||
Box(
|
||||
@ -260,14 +290,17 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
|
||||
) {
|
||||
var isSelected by remember { mutableStateOf(false) }
|
||||
isSelected = selectMode && episode.id in selectedIds
|
||||
LaunchedEffect(key1 = selectMode, key2 = selectedSize) {
|
||||
isSelected = selectMode && episode in selected
|
||||
// Logd(TAG, "LaunchedEffect $index $isSelected ${selected.size}")
|
||||
}
|
||||
fun toggleSelected() {
|
||||
isSelected = !isSelected
|
||||
if (isSelected) {
|
||||
selectedIds.add(episode.id)
|
||||
// selectedIds.add(episode.id)
|
||||
selected.add(episodes[index])
|
||||
} else {
|
||||
selectedIds.remove(episode.id)
|
||||
// selectedIds.remove(episode.id)
|
||||
selected.remove(episodes[index])
|
||||
}
|
||||
}
|
||||
@ -276,7 +309,7 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
if (false) {
|
||||
val typedValue = TypedValue()
|
||||
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
|
||||
Image(painter = painterResource(typedValue.resourceId),
|
||||
Icon(painter = painterResource(typedValue.resourceId), tint = textColor,
|
||||
contentDescription = "drag handle",
|
||||
modifier = Modifier.width(16.dp).align(Alignment.CenterVertically))
|
||||
}
|
||||
@ -284,7 +317,8 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
val (imgvCover, checkMark) = createRefs()
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode)
|
||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover",
|
||||
Modifier.width(56.dp).height(56.dp)
|
||||
placeholder = painterResource(R.mipmap.ic_launcher),
|
||||
modifier = Modifier.width(56.dp).height(56.dp)
|
||||
.constrainAs(imgvCover) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
@ -295,8 +329,8 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
else activity.loadChildFragment(FeedInfoFragment.newInstance(episode.feed!!))
|
||||
}))
|
||||
val alpha = if (playedState) 1.0f else 0f
|
||||
if (playedState) Image(painter = painterResource(R.drawable.ic_check), contentDescription = "played_mark",
|
||||
Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
|
||||
if (playedState) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark",
|
||||
modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) {
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(parent.end)
|
||||
})
|
||||
@ -310,18 +344,24 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selectedIds.add(episode.id)
|
||||
// selectedIds.add(episode.id)
|
||||
selected.add(episodes[index])
|
||||
} else selectedIds.clear()
|
||||
// selectedSize = selectedIds.size
|
||||
longPressIndex = index
|
||||
} else {
|
||||
// selectedIds.clear()
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
}
|
||||
Logd(TAG, "long clicked: ${episode.title}")
|
||||
})) {
|
||||
Row {
|
||||
if (episode.media?.getMediaType() == MediaType.VIDEO)
|
||||
Image(painter = painterResource(R.drawable.ic_videocam), contentDescription = "isVideo", Modifier.width(14.dp).height(14.dp))
|
||||
Icon(painter = painterResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
if (farvoriteState)
|
||||
Image(painter = painterResource(R.drawable.ic_star), contentDescription = "isFavorite", Modifier.width(14.dp).height(14.dp))
|
||||
if (curQueue.contains(episode))
|
||||
Image(painter = painterResource(R.drawable.ic_playlist_play), contentDescription = "ivInPlaylist", Modifier.width(14.dp).height(14.dp))
|
||||
Icon(painter = painterResource(R.drawable.ic_star), tint = textColor, contentDescription = "isFavorite", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
if (episode.inQueueState.value)
|
||||
Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
val dateSizeText = " · " + formatAbbrev(LocalContext.current, episode.getPubDate()) + " · " + if((episode.media?.size?:0) > 0) Formatter.formatShortFileSize(LocalContext.current, episode.media!!.size) else ""
|
||||
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
@ -339,19 +379,78 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
||||
}
|
||||
var actionButton by remember { mutableStateOf(if (actionButton_ == null) EpisodeActionButton.forItem(episodes[index]) else actionButton_(episodes[index])) }
|
||||
var showAltActionsDialog by remember { mutableStateOf(false) }
|
||||
val dls = remember { DownloadServiceInterface.get() }
|
||||
var dlPercent by remember { mutableIntStateOf(0) }
|
||||
fun isDownloading(): Boolean {
|
||||
return episodes[index].downloadState.value > DownloadStatus.State.UNKNOWN.ordinal && episodes[index].downloadState.value < DownloadStatus.State.COMPLETED.ordinal
|
||||
}
|
||||
if (actionButton_ == null) {
|
||||
LaunchedEffect(episodes[index].downloadState.value) {
|
||||
if (isDownloading()) dlPercent = dls?.getProgress(episodes[index].media!!.downloadUrl!!) ?: 0
|
||||
// Logd(TAG, "downloadState: ${episodes[index].downloadState.value} ${episode.media?.downloaded} $dlPercent")
|
||||
actionButton = EpisodeActionButton.forItem(episodes[index])
|
||||
}
|
||||
LaunchedEffect(episodes[index].isPlayingState.value) {
|
||||
Logd(TAG, "$index isPlayingState: ${episode.isPlayingState.value}")
|
||||
actionButton = EpisodeActionButton.forItem(episodes[index])
|
||||
}
|
||||
}
|
||||
Box(modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.CenterVertically).pointerInput(Unit) {
|
||||
detectTapGestures(onLongPress = { showAltActionsDialog = true }, onTap = {
|
||||
actionButton.onClick(activity)
|
||||
actionButton = EpisodeActionButton.forItem(episodes[index])
|
||||
})
|
||||
}, contentAlignment = Alignment.Center) {
|
||||
Image(painter = painterResource(actionButton.getDrawable()), contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
|
||||
Icon(painter = painterResource(actionButton.getDrawable()), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp))
|
||||
if (isDownloading() && dlPercent >= 0) CircularProgressIndicator(progress = 0.01f * dlPercent, strokeWidth = 4.dp, color = textColor)
|
||||
}
|
||||
if (showAltActionsDialog) actionButton.AltActionsDialog(activity, showAltActionsDialog, onDismiss = { showAltActionsDialog = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectMode) EpisodeSpeedDial(activity, selected, modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
|
||||
if (selectMode) {
|
||||
Row(modifier = Modifier.align(Alignment.TopEnd).width(150.dp).height(45.dp).background(Color.LightGray), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(painter = painterResource(R.drawable.baseline_arrow_upward_24), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp)
|
||||
.clickable(onClick = {
|
||||
// selectedIds.clear()
|
||||
selected.clear()
|
||||
for (i in 0..longPressIndex) {
|
||||
// selectedIds.add(episodes[i].id)
|
||||
selected.add(episodes[i])
|
||||
}
|
||||
selectedSize = selected.size
|
||||
Logd(TAG, "selectedIds: ${selected.size}")
|
||||
}))
|
||||
Icon(painter = painterResource(R.drawable.baseline_arrow_downward_24), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp)
|
||||
.clickable(onClick = {
|
||||
// selectedIds.clear()
|
||||
selected.clear()
|
||||
for (i in longPressIndex..episodes.size-1) {
|
||||
// selectedIds.add(episodes[i].id)
|
||||
selected.add(episodes[i])
|
||||
}
|
||||
selectedSize = selected.size
|
||||
Logd(TAG, "selectedIds: ${selected.size}")
|
||||
}))
|
||||
var selectAllRes by remember { mutableIntStateOf(R.drawable.ic_select_all) }
|
||||
Icon(painter = painterResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)
|
||||
.clickable(onClick = {
|
||||
if (selectedSize != episodes.size) {
|
||||
for (e in episodes) {
|
||||
// selectedIds.add(e.id)
|
||||
selected.add(e)
|
||||
}
|
||||
selectAllRes = R.drawable.ic_select_none
|
||||
} else {
|
||||
// selectedIds.clear()
|
||||
selected.clear()
|
||||
selectAllRes = R.drawable.ic_select_all
|
||||
}
|
||||
selectedSize = selected.size
|
||||
Logd(TAG, "selectedIds: ${selected.size}")
|
||||
}))
|
||||
}
|
||||
EpisodeSpeedDial(activity, selected.toMutableStateList(), modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import java.util.*
|
||||
* Shows the dialog that allows setting the skip time.
|
||||
*/
|
||||
object SkipPreferenceDialog {
|
||||
fun showSkipPreference(context: Context, direction: SkipDirection, textView: TextView?) {
|
||||
fun showSkipPreference(context: Context, direction: SkipDirection, textView: TextView? = null) {
|
||||
var checked = 0
|
||||
val skipSecs = if (direction == SkipDirection.SKIP_FORWARD) fastForwardSecs else rewindSecs
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,8 @@ package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.BaseEpisodesListFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus.State.UNKNOWN
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
@ -102,45 +104,6 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
||||
emptyView.setMessage(R.string.no_all_episodes_label)
|
||||
emptyView.hide()
|
||||
|
||||
// val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
// speedDialView = multiSelectDial.fabSD
|
||||
// speedDialView.overlayLayout = multiSelectDial.fabSDOverlay
|
||||
// speedDialView.inflate(R.menu.episodes_apply_action_speeddial)
|
||||
// speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener {
|
||||
// override fun onMainActionSelected(): Boolean {
|
||||
// return false
|
||||
// }
|
||||
// override fun onToggleChanged(open: Boolean) {
|
||||
// if (open && adapter.selectedCount == 0) {
|
||||
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT)
|
||||
// speedDialView.close()
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
|
||||
//// if (actionItem.id == R.id.put_in_queue_batch) {
|
||||
//// EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(adapter.selectedItems.filterIsInstance<Episode>())
|
||||
//// true
|
||||
//// } else {
|
||||
// var confirmationString = 0
|
||||
// Logd(TAG, "adapter.selectedItems: ${adapter.selectedItems.size}")
|
||||
// if (adapter.selectedItems.size >= 25 || adapter.shouldSelectLazyLoadedItems()) {
|
||||
// when (actionItem.id) {
|
||||
// R.id.toggle_played_batch -> confirmationString = R.string.multi_select_toggle_played_confirmation
|
||||
// else -> confirmationString = R.string.multi_select_operation_confirmation
|
||||
// }
|
||||
// }
|
||||
// if (confirmationString == 0) performMultiSelectAction(actionItem.id)
|
||||
// else {
|
||||
// object : ConfirmationDialog(activity as MainActivity, R.string.multi_select, confirmationString) {
|
||||
// override fun onConfirmButtonPressed(dialog: DialogInterface) {
|
||||
// performMultiSelectAction(actionItem.id)
|
||||
// }
|
||||
// }.createNewDialog().show()
|
||||
// }
|
||||
// true
|
||||
//// }
|
||||
// }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ -194,10 +157,12 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
||||
|
||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
if (loadItemsRunning) return
|
||||
// for (downloadUrl in event.urls) {
|
||||
// val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl)
|
||||
// if (pos >= 0) adapter.notifyItemChangedCompat(pos)
|
||||
// }
|
||||
for (url in event.urls) {
|
||||
// if (!event.isCompleted(url)) continue
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, url)
|
||||
if (pos >= 0) episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
|
@ -81,7 +81,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
loadMediaInfo(true)
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
@ -106,19 +105,15 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||
}
|
||||
})
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
progressBar.visibility = View.VISIBLE
|
||||
|
||||
val wrapHeight = CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT)
|
||||
recyclerView.layoutParams = wrapHeight
|
||||
|
||||
controller = object : ServiceStatusHandler(requireActivity()) {
|
||||
override fun loadMediaInfo() {
|
||||
this@ChaptersFragment.loadMediaInfo(false)
|
||||
}
|
||||
}
|
||||
controller?.init()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ -167,7 +162,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||
|
||||
private fun getCurrentChapter(media: Playable?): Int {
|
||||
if (controller == null) return -1
|
||||
|
||||
return getCurrentChapterIndex(media, curPositionFB)
|
||||
}
|
||||
|
||||
@ -192,7 +186,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||
dismiss()
|
||||
Toast.makeText(context, R.string.no_chapters_label, Toast.LENGTH_LONG).show()
|
||||
} else progressBar.visibility = View.GONE
|
||||
|
||||
adapter.setMedia(media)
|
||||
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_NEUTRAL).visibility = View.INVISIBLE
|
||||
if ((media is EpisodeMedia) && !media.episodeOrFetch()?.podcastIndexChapterUrl.isNullOrEmpty())
|
||||
@ -214,7 +207,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||
}
|
||||
|
||||
class ChaptersListAdapter(private val context: Context, private val callback: Callback?) : RecyclerView.Adapter<ChaptersListAdapter.ChapterHolder>() {
|
||||
|
||||
private var media: Playable? = null
|
||||
private var currentChapterIndex = -1
|
||||
private var currentChapterPosition: Long = -1
|
||||
|
@ -164,7 +164,7 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To
|
||||
}
|
||||
|
||||
private class DownloadLogAdapter(private val context: Activity) : BaseAdapter() {
|
||||
private var downloadLog: List<DownloadResult> = ArrayList()
|
||||
private var downloadLog: List<DownloadResult> = listOf()
|
||||
|
||||
fun setDownloadLog(downloadLog: List<DownloadResult>) {
|
||||
this.downloadLog = downloadLog
|
||||
|
@ -0,0 +1,929 @@
|
||||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.*
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils
|
||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.text.TextUtils
|
||||
import android.text.format.Formatter.formatShortFileSize
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.skydoves.balloon.ArrowOrientation
|
||||
import com.skydoves.balloon.ArrowOrientationRules
|
||||
import com.skydoves.balloon.Balloon
|
||||
import com.skydoves.balloon.BalloonAnimation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.dankito.readability4j.extended.Readability4JExtended
|
||||
import okhttp3.Request.Builder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Displays information about an Episode (FeedItem) and actions.
|
||||
*/
|
||||
@UnstableApi
|
||||
class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
private var _binding: EpisodeInfoFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var homeFragment: EpisodeHomeFragment? = null
|
||||
|
||||
private var itemLoaded = false
|
||||
private var episode: Episode? = null // managed
|
||||
|
||||
private var webviewData by mutableStateOf("")
|
||||
|
||||
private lateinit var shownotesCleaner: ShownotesCleaner
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
// private lateinit var webvDescription: ShownotesWebView
|
||||
// private lateinit var imgvCover: ImageView
|
||||
|
||||
// private lateinit var butAction1: ImageView
|
||||
// private lateinit var butAction2: ImageView
|
||||
|
||||
private var actionButton1 by mutableStateOf<EpisodeActionButton?>(null)
|
||||
private var actionButton2 by mutableStateOf<EpisodeActionButton?>(null)
|
||||
|
||||
@UnstableApi
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
_binding = EpisodeInfoFragmentBinding.inflate(inflater, container, false)
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
|
||||
toolbar = binding.toolbar
|
||||
toolbar.title = ""
|
||||
toolbar.inflateMenu(R.menu.feeditem_options)
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
binding.composeView.setContent{
|
||||
CustomTheme(requireContext()) {
|
||||
InfoView()
|
||||
}
|
||||
}
|
||||
|
||||
// binding.txtvPodcast.setOnClickListener { openPodcast() }
|
||||
// binding.txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
||||
// binding.txtvTitle.ellipsize = TextUtils.TruncateAt.END
|
||||
// webvDescription = binding.webvDescription
|
||||
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||
// val cMedia = curMedia
|
||||
// if (episode?.media?.getIdentifier() == cMedia?.getIdentifier()) seekTo(time ?: 0)
|
||||
// else (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position, Snackbar.LENGTH_LONG)
|
||||
// }
|
||||
// registerForContextMenu(webvDescription)
|
||||
|
||||
// imgvCover = binding.imgvCover
|
||||
// imgvCover.setOnClickListener { openPodcast() }
|
||||
// butAction1 = binding.butAction1
|
||||
// butAction2 = binding.butAction2
|
||||
|
||||
// binding.homeButton.setOnClickListener {
|
||||
// if (!episode?.link.isNullOrEmpty()) {
|
||||
// homeFragment = EpisodeHomeFragment.newInstance(episode!!)
|
||||
// (activity as MainActivity).loadChildFragment(homeFragment!!)
|
||||
// } else Toast.makeText(context, "Episode link is not valid ${episode?.link}", Toast.LENGTH_LONG).show()
|
||||
// }
|
||||
|
||||
// butAction1.setOnClickListener(View.OnClickListener {
|
||||
// when {
|
||||
// actionButton1 is StreamActionButton && !UserPreferences.isStreamOverDownload
|
||||
// && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_STREAM) -> {
|
||||
// showOnDemandConfigBalloon(true)
|
||||
// return@OnClickListener
|
||||
// }
|
||||
// actionButton1 == null -> return@OnClickListener // Not loaded yet
|
||||
// else -> actionButton1?.onClick(requireContext())
|
||||
// }
|
||||
// })
|
||||
// butAction2.setOnClickListener(View.OnClickListener {
|
||||
// when {
|
||||
// actionButton2 is DownloadActionButton && UserPreferences.isStreamOverDownload
|
||||
// && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_DOWNLOAD) -> {
|
||||
// showOnDemandConfigBalloon(false)
|
||||
// return@OnClickListener
|
||||
// }
|
||||
// actionButton2 == null -> return@OnClickListener // Not loaded yet
|
||||
// else -> actionButton2?.onClick(requireContext())
|
||||
// }
|
||||
// })
|
||||
shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
onFragmentLoaded()
|
||||
load()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoView() {
|
||||
Column {
|
||||
val textColor = MaterialTheme.colors.onSurface
|
||||
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
val imgLoc = if (episode != null) ImageResourceUtils.getEpisodeListImageLocation(episode!!) else null
|
||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover", Modifier.width(56.dp).height(56.dp).clickable(onClick = { openPodcast() }))
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.body1, modifier = Modifier.clickable { openPodcast() })
|
||||
Text(txtvTitle, color = textColor, style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Bold), maxLines = 5, overflow = TextOverflow.Ellipsis)
|
||||
Text(txtvPublished + " · " + txtvDuration + " · " + txtvSize, color = textColor, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (hasMedia) Icon(painter = painterResource(actionButton1?.getDrawable()?: R.drawable.ic_questionmark), tint = textColor, contentDescription = "butAction1",
|
||||
modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = {
|
||||
when {
|
||||
actionButton1 is StreamActionButton && !UserPreferences.isStreamOverDownload
|
||||
&& UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_STREAM) -> {
|
||||
showOnDemandConfigBalloon(true)
|
||||
return@clickable
|
||||
}
|
||||
actionButton1 == null -> return@clickable // Not loaded yet
|
||||
else -> actionButton1?.onClick(requireContext())
|
||||
}
|
||||
}))
|
||||
Spacer(modifier = Modifier.weight(0.2f))
|
||||
Icon(painter = painterResource(R.drawable.baseline_home_work_24), tint = textColor, contentDescription = "homeButton",
|
||||
modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = {
|
||||
if (!episode?.link.isNullOrEmpty()) {
|
||||
homeFragment = EpisodeHomeFragment.newInstance(episode!!)
|
||||
(activity as MainActivity).loadChildFragment(homeFragment!!)
|
||||
} else Toast.makeText(context, "Episode link is not valid ${episode?.link}", Toast.LENGTH_LONG).show()
|
||||
}))
|
||||
Spacer(modifier = Modifier.weight(0.2f))
|
||||
Box(modifier = Modifier.width(40.dp).height(40.dp).align(Alignment.CenterVertically), contentAlignment = Alignment.Center) {
|
||||
Icon(painter = painterResource(actionButton2?.getDrawable()?: R.drawable.ic_questionmark), tint = textColor, contentDescription = "butAction2", modifier = Modifier.width(24.dp).height(24.dp).clickable {
|
||||
when {
|
||||
actionButton2 is DownloadActionButton && UserPreferences.isStreamOverDownload
|
||||
&& UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_DOWNLOAD) -> {
|
||||
showOnDemandConfigBalloon(false)
|
||||
return@clickable
|
||||
}
|
||||
actionButton2 == null -> return@clickable // Not loaded yet
|
||||
else -> actionButton2?.onClick(requireContext())
|
||||
}
|
||||
})
|
||||
// if (cond) CircularProgressIndicator(progress = 0.01f * dlPercent, strokeWidth = 4.dp, color = textColor)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
if (!hasMedia) Text("noMediaLabel", color = textColor, style = MaterialTheme.typography.body2)
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||
AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
|
||||
ShownotesWebView(context).apply {
|
||||
setTimecodeSelectedListener { time: Int -> seekTo(time) }
|
||||
setPageFinishedListener {
|
||||
// Restoring the scroll position might not always work
|
||||
postDelayed({ }, 50)
|
||||
}
|
||||
}
|
||||
}, update = {
|
||||
it.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank")
|
||||
})
|
||||
Text(itemLink, color = textColor, style = MaterialTheme.typography.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun showOnDemandConfigBalloon(offerStreaming: Boolean) {
|
||||
val isLocaleRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL)
|
||||
val balloon: Balloon = Balloon.Builder(requireContext())
|
||||
.setArrowOrientation(ArrowOrientation.TOP)
|
||||
.setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED)
|
||||
.setArrowPosition(0.25f + (if (isLocaleRtl xor offerStreaming) 0f else 0.5f))
|
||||
.setWidthRatio(1.0f)
|
||||
.setMarginLeft(8)
|
||||
.setMarginRight(8)
|
||||
.setBackgroundColor(ThemeUtils.getColorFromAttr(requireContext(), com.google.android.material.R.attr.colorSecondary))
|
||||
.setBalloonAnimation(BalloonAnimation.OVERSHOOT)
|
||||
.setLayout(R.layout.popup_bubble_view)
|
||||
.setDismissWhenTouchOutside(true)
|
||||
.setLifecycleOwner(this)
|
||||
.build()
|
||||
val ballonView = balloon.getContentView()
|
||||
val positiveButton: Button = ballonView.findViewById(R.id.balloon_button_positive)
|
||||
val negativeButton: Button = ballonView.findViewById(R.id.balloon_button_negative)
|
||||
val message: TextView = ballonView.findViewById(R.id.balloon_message)
|
||||
message.setText(if (offerStreaming) R.string.on_demand_config_stream_text
|
||||
else R.string.on_demand_config_download_text)
|
||||
positiveButton.setOnClickListener {
|
||||
UserPreferences.isStreamOverDownload = offerStreaming
|
||||
// Update all visible lists to reflect new streaming action button
|
||||
// TODO: need another event type?
|
||||
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent())
|
||||
(activity as MainActivity).showSnackbarAbovePlayer(R.string.on_demand_config_setting_changed, Snackbar.LENGTH_SHORT)
|
||||
balloon.dismiss()
|
||||
}
|
||||
negativeButton.setOnClickListener {
|
||||
UsageStatistics.doNotAskAgain(UsageStatistics.ACTION_STREAM) // Type does not matter. Both are silenced.
|
||||
balloon.dismiss()
|
||||
}
|
||||
// balloon.showAlignBottom(butAction1, 0, (-12 * resources.displayMetrics.density).toInt())
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.share_notes -> {
|
||||
if (episode == null) return false
|
||||
val notes = episode!!.description
|
||||
if (!notes.isNullOrEmpty()) {
|
||||
val shareText = if (Build.VERSION.SDK_INT >= 24) HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
else HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
val context = requireContext()
|
||||
val intent = ShareCompat.IntentBuilder(context)
|
||||
.setType("text/plain")
|
||||
.setText(shareText)
|
||||
.setChooserTitle(R.string.share_notes_label)
|
||||
.createChooserIntent()
|
||||
context.startActivity(intent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
if (episode == null) return false
|
||||
return EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, episode!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (itemLoaded) {
|
||||
// binding.progbarLoading.visibility = View.GONE
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
// binding.root.removeView(webvDescription)
|
||||
episode = null
|
||||
// webvDescription.clearHistory()
|
||||
// webvDescription.clearCache(true)
|
||||
// webvDescription.clearView()
|
||||
// webvDescription.destroy()
|
||||
_binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun onFragmentLoaded() {
|
||||
// if (!itemLoaded)
|
||||
// webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank")
|
||||
// if (item?.link != null) binding.webView.loadUrl(item!!.link!!)
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
private fun prepareMenu() {
|
||||
if (episode!!.media != null) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast)
|
||||
// these are already available via button1 and button2
|
||||
else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, episode, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
|
||||
}
|
||||
|
||||
private var txtvPodcast by mutableStateOf("")
|
||||
private var txtvTitle by mutableStateOf("")
|
||||
private var txtvPublished by mutableStateOf("")
|
||||
private var txtvSize by mutableStateOf("")
|
||||
private var txtvDuration by mutableStateOf("")
|
||||
private var itemLink by mutableStateOf("")
|
||||
@UnstableApi
|
||||
private fun updateAppearance() {
|
||||
if (episode == null) {
|
||||
Logd(TAG, "updateAppearance item is null")
|
||||
return
|
||||
}
|
||||
prepareMenu()
|
||||
|
||||
if (episode!!.feed != null) txtvPodcast = episode!!.feed!!.title ?: ""
|
||||
txtvTitle = episode!!.title ?:""
|
||||
itemLink = episode!!.link?: ""
|
||||
|
||||
if (episode?.pubDate != null) {
|
||||
val pubDateStr = formatAbbrev(context, Date(episode!!.pubDate))
|
||||
txtvPublished = pubDateStr
|
||||
// binding.txtvPublished.setContentDescription(formatForAccessibility(Date(episode!!.pubDate)))
|
||||
}
|
||||
|
||||
val media = episode?.media
|
||||
when {
|
||||
media == null -> txtvSize = ""
|
||||
media.size > 0 -> txtvSize = formatShortFileSize(activity, media.size)
|
||||
isEpisodeHeadDownloadAllowed && !media.checkedOnSizeButUnknown() -> {
|
||||
txtvSize = "{faw_spinner}"
|
||||
// Iconify.addIcons(size)
|
||||
lifecycleScope.launch {
|
||||
val sizeValue = getMediaSize(episode)
|
||||
txtvSize = if (sizeValue <= 0) "" else formatShortFileSize(activity, sizeValue)
|
||||
}
|
||||
}
|
||||
else -> txtvSize = ""
|
||||
}
|
||||
|
||||
// val imgLocFB = ImageResourceUtils.getFallbackImageLocation(episode!!)
|
||||
// val imageLoader = imgvCover.context.imageLoader
|
||||
// val imageRequest = ImageRequest.Builder(requireContext())
|
||||
// .data(episode!!.imageLocation)
|
||||
// .placeholder(R.color.light_gray)
|
||||
// .listener(object : ImageRequest.Listener {
|
||||
// override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
// val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||
// .data(imgLocFB)
|
||||
// .setHeader("User-Agent", "Mozilla/5.0")
|
||||
// .error(R.mipmap.ic_launcher)
|
||||
// .target(imgvCover)
|
||||
// .build()
|
||||
// imageLoader.enqueue(fallbackImageRequest)
|
||||
// }
|
||||
// })
|
||||
// .target(imgvCover)
|
||||
// .build()
|
||||
// imageLoader.enqueue(imageRequest)
|
||||
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
var hasMedia by mutableStateOf(true)
|
||||
@UnstableApi
|
||||
private fun updateButtons() {
|
||||
// binding.circularProgressBar.visibility = View.GONE
|
||||
val dls = DownloadServiceInterface.get()
|
||||
if (episode != null && episode!!.media != null && episode!!.media!!.downloadUrl != null) {
|
||||
val url = episode!!.media!!.downloadUrl!!
|
||||
if (dls != null && dls.isDownloadingEpisode(url)) {
|
||||
// binding.circularProgressBar.visibility = View.VISIBLE
|
||||
// binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), episode)
|
||||
// binding.circularProgressBar.setIndeterminate(dls.isEpisodeQueued(url))
|
||||
}
|
||||
}
|
||||
|
||||
val media: EpisodeMedia? = episode?.media
|
||||
if (media == null) {
|
||||
if (episode != null) {
|
||||
// actionButton1 = VisitWebsiteActionButton(episode!!)
|
||||
// butAction1.visibility = View.INVISIBLE
|
||||
actionButton2 = VisitWebsiteActionButton(episode!!)
|
||||
}
|
||||
hasMedia = false
|
||||
} else {
|
||||
hasMedia = true
|
||||
if (media.getDuration() > 0) {
|
||||
txtvDuration = DurationConverter.getDurationStringLong(media.getDuration())
|
||||
// binding.txtvDuration.setContentDescription(DurationConverter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
|
||||
}
|
||||
if (episode != null) {
|
||||
actionButton1 = when {
|
||||
// media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(episode!!)
|
||||
InTheatre.isCurrentlyPlaying(media) -> PauseActionButton(episode!!)
|
||||
episode!!.feed != null && episode!!.feed!!.isLocalFeed -> PlayLocalActionButton(episode!!)
|
||||
media.downloaded -> PlayActionButton(episode!!)
|
||||
else -> StreamActionButton(episode!!)
|
||||
}
|
||||
actionButton2 = when {
|
||||
episode!!.feed?.type == Feed.FeedType.YOUTUBE.name -> VisitWebsiteActionButton(episode!!)
|
||||
dls != null && media.downloadUrl != null && dls.isDownloadingEpisode(media.downloadUrl!!) -> CancelDownloadActionButton(episode!!)
|
||||
!media.downloaded -> DownloadActionButton(episode!!)
|
||||
else -> DeleteActionButton(episode!!)
|
||||
}
|
||||
// if (actionButton2 != null && media.getMediaType() == MediaType.FLASH) actionButton2!!.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if (actionButton1 != null) {
|
||||
// butAction1.setImageResource(actionButton1!!.getDrawable())
|
||||
// butAction1.visibility = actionButton1!!.visibility
|
||||
}
|
||||
if (actionButton2 != null) {
|
||||
// butAction2.setImageResource(actionButton2!!.getDrawable())
|
||||
// butAction2.visibility = actionButton2!!.visibility
|
||||
}
|
||||
}
|
||||
|
||||
// override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
// return webvDescription.onContextItemSelected(item)
|
||||
// }
|
||||
|
||||
@OptIn(UnstableApi::class) private fun openPodcast() {
|
||||
if (episode?.feedId == null) return
|
||||
|
||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(episode!!.feedId!!)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private var eventStickySink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
eventStickySink?.cancel()
|
||||
eventStickySink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink == null) eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.QueueEvent -> onQueueEvent(event)
|
||||
is FlowEvent.FavoritesEvent -> onFavoriteEvent(event)
|
||||
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
||||
is FlowEvent.PlayerSettingsEvent -> updateButtons()
|
||||
is FlowEvent.EpisodePlayedEvent -> load()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eventStickySink == null) eventStickySink = lifecycleScope.launch {
|
||||
EventFlow.stickyEvents.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFavoriteEvent(event: FlowEvent.FavoritesEvent) {
|
||||
if (episode?.id == event.episode.id) {
|
||||
episode = unmanaged(episode!!)
|
||||
episode!!.isFavorite = event.episode.isFavorite
|
||||
// episode = event.episode
|
||||
prepareMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
|
||||
var i = 0
|
||||
val size: Int = event.episodes.size
|
||||
while (i < size) {
|
||||
val item_ = event.episodes[i]
|
||||
if (item_.id == episode?.id) {
|
||||
// episode = unmanaged(item_)
|
||||
// episode = item_
|
||||
prepareMenu()
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
|
||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||
if (this.episode == null) return
|
||||
for (item in event.episodes) {
|
||||
if (this.episode!!.id == item.id) {
|
||||
load()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
if (episode == null || episode!!.media == null) return
|
||||
if (!event.urls.contains(episode!!.media!!.downloadUrl)) return
|
||||
if (itemLoaded && activity != null) updateButtons()
|
||||
}
|
||||
|
||||
private var loadItemsRunning = false
|
||||
@UnstableApi
|
||||
private fun load() {
|
||||
// if (!itemLoaded) binding.progbarLoading.visibility = View.VISIBLE
|
||||
Logd(TAG, "load() called")
|
||||
if (!loadItemsRunning) {
|
||||
loadItemsRunning = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (episode != null) {
|
||||
val duration = episode!!.media?.getDuration() ?: Int.MAX_VALUE
|
||||
Logd(TAG, "description: ${episode?.description}")
|
||||
val url = episode!!.media?.downloadUrl
|
||||
if (url?.contains("youtube.com") == true && episode!!.description?.startsWith("Short:") == true) {
|
||||
Logd(TAG, "getting extended description: ${episode!!.title}")
|
||||
try {
|
||||
val info = episode!!.streamInfo
|
||||
if (info?.description?.content != null) {
|
||||
episode = upsert(episode!!) { it.description = info.description?.content }
|
||||
webviewData = shownotesCleaner.processShownotes(info.description!!.content, duration)
|
||||
} else webviewData = shownotesCleaner.processShownotes(episode!!.description ?: "", duration)
|
||||
} catch (e: Exception) { Logd(TAG, "StreamInfo error: ${e.message}") }
|
||||
} else webviewData = shownotesCleaner.processShownotes(episode!!.description ?: "", duration)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
// binding.progbarLoading.visibility = View.GONE
|
||||
onFragmentLoaded()
|
||||
itemLoaded = true
|
||||
}
|
||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||
} finally { loadItemsRunning = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setItem(item_: Episode) {
|
||||
episode = item_
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays information about an Episode (FeedItem) and actions.
|
||||
*/
|
||||
class EpisodeHomeFragment : Fragment() {
|
||||
private var _binding: EpisodeHomeFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var startIndex = 0
|
||||
private var ttsSpeed = 1.0f
|
||||
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
private var readerText: String? = null
|
||||
private var cleanedNotes: String? = null
|
||||
private var readerhtml: String? = null
|
||||
private var readMode = true
|
||||
private var ttsPlaying = false
|
||||
private var jsEnabled = false
|
||||
|
||||
private var tts: TextToSpeech? = null
|
||||
private var ttsReady = false
|
||||
|
||||
@UnstableApi
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
_binding = EpisodeHomeFragmentBinding.inflate(inflater, container, false)
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
toolbar = binding.toolbar
|
||||
toolbar.title = ""
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.addMenuProvider(menuProvider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
if (!episode?.link.isNullOrEmpty()) showContent()
|
||||
else {
|
||||
Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
binding.webView.apply {
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
val isEmpty = view?.title.isNullOrEmpty() && view?.contentDescription.isNullOrEmpty()
|
||||
if (isEmpty) Logd(TAG, "content is empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
updateAppearance()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun switchMode() {
|
||||
readMode = !readMode
|
||||
showContent()
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun showReaderContent() {
|
||||
runOnIOScope {
|
||||
if (!episode?.link.isNullOrEmpty()) {
|
||||
if (cleanedNotes == null) {
|
||||
if (episode?.transcript == null) {
|
||||
val url = episode!!.link!!
|
||||
val htmlSource = fetchHtmlSource(url)
|
||||
val article = Readability4JExtended(episode?.link!!, htmlSource).parse()
|
||||
readerText = article.textContent
|
||||
// Log.d(TAG, "readability4J: ${article.textContent}")
|
||||
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||
} else {
|
||||
readerhtml = episode!!.transcript
|
||||
readerText = HtmlCompat.fromHtml(readerhtml!!, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
}
|
||||
if (!readerhtml.isNullOrEmpty()) {
|
||||
val shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
cleanedNotes = shownotesCleaner.processShownotes(readerhtml!!, 0)
|
||||
episode = upsertBlk(episode!!) {
|
||||
it.setTranscriptIfLonger(readerhtml)
|
||||
}
|
||||
// persistEpisode(episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cleanedNotes.isNullOrEmpty()) {
|
||||
if (!ttsReady) initializeTTS(requireContext())
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.readerView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes ?: "No notes",
|
||||
"text/html", "UTF-8", null)
|
||||
binding.readerView.visibility = View.VISIBLE
|
||||
binding.webView.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeTTS(context: Context) {
|
||||
Logd(TAG, "starting TTS")
|
||||
if (tts == null) {
|
||||
tts = TextToSpeech(context) { status: Int ->
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
if (episode?.feed?.language != null) {
|
||||
val result = tts?.setLanguage(Locale(episode!!.feed!!.language!!))
|
||||
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
Log.w(TAG, "TTS language not supported ${episode?.feed?.language}")
|
||||
requireActivity().runOnUiThread {
|
||||
Toast.makeText(context, getString(R.string.language_not_supported_by_tts) + " ${episode?.feed?.language}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
ttsReady = true
|
||||
// semaphore.release()
|
||||
Logd(TAG, "TTS init success")
|
||||
} else {
|
||||
Log.w(TAG, "TTS init failed")
|
||||
requireActivity().runOnUiThread { Toast.makeText(context, R.string.tts_init_failed, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showWebContent() {
|
||||
if (!episode?.link.isNullOrEmpty()) {
|
||||
binding.webView.settings.javaScriptEnabled = jsEnabled
|
||||
Logd(TAG, "currentItem!!.link ${episode!!.link}")
|
||||
binding.webView.loadUrl(episode!!.link!!)
|
||||
binding.readerView.visibility = View.GONE
|
||||
binding.webView.visibility = View.VISIBLE
|
||||
} else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun showContent() {
|
||||
if (readMode) showReaderContent()
|
||||
else showWebContent()
|
||||
}
|
||||
|
||||
private val menuProvider = object: MenuProvider {
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
// super.onPrepareMenu(menu)
|
||||
Logd(TAG, "onPrepareMenu called")
|
||||
val textSpeech = menu.findItem(R.id.text_speech)
|
||||
textSpeech?.isVisible = readMode && tts != null
|
||||
if (textSpeech?.isVisible == true) {
|
||||
if (ttsPlaying) textSpeech.setIcon(R.drawable.ic_pause) else textSpeech.setIcon(R.drawable.ic_play_24dp)
|
||||
}
|
||||
menu.findItem(R.id.share_notes)?.setVisible(readMode)
|
||||
menu.findItem(R.id.switchJS)?.setVisible(!readMode)
|
||||
val btn = menu.findItem(R.id.switch_home)
|
||||
if (readMode) btn?.setIcon(R.drawable.baseline_home_24)
|
||||
else btn?.setIcon(R.drawable.outline_home_24)
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.episode_home, menu)
|
||||
onPrepareMenu(menu)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.switch_home -> {
|
||||
switchMode()
|
||||
return true
|
||||
}
|
||||
R.id.switchJS -> {
|
||||
jsEnabled = !jsEnabled
|
||||
showWebContent()
|
||||
return true
|
||||
}
|
||||
R.id.text_speech -> {
|
||||
Logd(TAG, "text_speech selected: $readerText")
|
||||
if (tts != null) {
|
||||
if (tts!!.isSpeaking) tts?.stop()
|
||||
if (!ttsPlaying) {
|
||||
ttsPlaying = true
|
||||
if (!readerText.isNullOrEmpty()) {
|
||||
ttsSpeed = episode?.feed?.preferences?.playSpeed ?: 1.0f
|
||||
tts?.setSpeechRate(ttsSpeed)
|
||||
while (startIndex < readerText!!.length) {
|
||||
val endIndex = minOf(startIndex + MAX_CHUNK_LENGTH, readerText!!.length)
|
||||
val chunk = readerText!!.substring(startIndex, endIndex)
|
||||
tts?.speak(chunk, TextToSpeech.QUEUE_ADD, null, null)
|
||||
startIndex += MAX_CHUNK_LENGTH
|
||||
}
|
||||
}
|
||||
} else ttsPlaying = false
|
||||
updateAppearance()
|
||||
} else Toast.makeText(context, R.string.tts_not_available, Toast.LENGTH_LONG).show()
|
||||
|
||||
return true
|
||||
}
|
||||
R.id.share_notes -> {
|
||||
val notes = readerhtml
|
||||
if (!notes.isNullOrEmpty()) {
|
||||
val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
val context = requireContext()
|
||||
val intent = ShareCompat.IntentBuilder(context)
|
||||
.setType("text/plain")
|
||||
.setText(shareText)
|
||||
.setChooserTitle(R.string.share_notes_label)
|
||||
.createChooserIntent()
|
||||
context.startActivity(intent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
return episode != null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
private fun cleatWebview(webview: WebView) {
|
||||
binding.root.removeView(webview)
|
||||
webview.clearHistory()
|
||||
webview.clearCache(true)
|
||||
webview.clearView()
|
||||
webview.destroy()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
cleatWebview(binding.webView)
|
||||
cleatWebview(binding.readerView)
|
||||
_binding = null
|
||||
tts?.stop()
|
||||
tts?.shutdown()
|
||||
tts = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun updateAppearance() {
|
||||
if (episode == null) {
|
||||
Logd(TAG, "updateAppearance currentItem is null")
|
||||
return
|
||||
}
|
||||
// onPrepareOptionsMenu(toolbar.menu)
|
||||
toolbar.invalidateMenu()
|
||||
// menuProvider.onPrepareMenu(toolbar.menu)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = EpisodeHomeFragment::class.simpleName ?: "Anonymous"
|
||||
private const val MAX_CHUNK_LENGTH = 2000
|
||||
|
||||
var episode: Episode? = null // unmanged
|
||||
|
||||
fun newInstance(item: Episode): EpisodeHomeFragment {
|
||||
val fragment = EpisodeHomeFragment()
|
||||
Logd(TAG, "item.itemIdentifier ${item.identifier}")
|
||||
if (item.identifier != episode?.identifier) episode = item
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = EpisodeInfoFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
private suspend fun getMediaSize(episode: Episode?) : Long {
|
||||
return withContext(Dispatchers.IO) {
|
||||
if (!isEpisodeHeadDownloadAllowed) return@withContext -1
|
||||
val media = episode?.media ?: return@withContext -1
|
||||
|
||||
var size = Int.MIN_VALUE.toLong()
|
||||
when {
|
||||
media.downloaded -> {
|
||||
val url = media.getLocalMediaUrl()
|
||||
if (!url.isNullOrEmpty()) {
|
||||
val mediaFile = File(url)
|
||||
if (mediaFile.exists()) size = mediaFile.length()
|
||||
}
|
||||
}
|
||||
!media.checkedOnSizeButUnknown() -> {
|
||||
// only query the network if we haven't already checked
|
||||
|
||||
val url = media.downloadUrl
|
||||
if (url.isNullOrEmpty()) return@withContext -1
|
||||
|
||||
val client = getHttpClient()
|
||||
val httpReq: Builder = Builder().url(url).header("Accept-Encoding", "identity").head()
|
||||
try {
|
||||
val response = client.newCall(httpReq.build()).execute()
|
||||
if (response.isSuccessful) {
|
||||
val contentLength = response.header("Content-Length")?:"0"
|
||||
try {
|
||||
size = contentLength.toInt().toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
return@withContext -1 // better luck next time
|
||||
}
|
||||
}
|
||||
}
|
||||
// they didn't tell us the size, but we don't want to keep querying on it
|
||||
upsert(episode) {
|
||||
if (size <= 0) it.media?.setCheckedOnSizeButUnknown()
|
||||
else it.media?.size = size
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
|
||||
fun newInstance(item: Episode): EpisodeInfoFragment {
|
||||
val fragment = EpisodeInfoFragment()
|
||||
fragment.setItem(item)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,8 @@ package ac.mdiq.podcini.ui.fragment
|
||||
//import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FeedItemListFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.MoreContentListFooterBinding
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus.State.UNKNOWN
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||
@ -14,13 +15,10 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.compose.FeedEpisodesHeader
|
||||
@ -74,6 +72,8 @@ import java.util.concurrent.Semaphore
|
||||
private var feedID: Long = 0
|
||||
private var feed by mutableStateOf<Feed?>(null)
|
||||
private val episodes = mutableStateListOf<Episode>()
|
||||
private var ieMap: Map<Long, Int> = mapOf()
|
||||
private var ueMap: Map<String, Int> = mapOf()
|
||||
|
||||
private var enableFilter: Boolean = true
|
||||
private var filterButColor = mutableStateOf(Color.White)
|
||||
@ -133,6 +133,8 @@ import java.util.concurrent.Semaphore
|
||||
if (sortOrder != null) getPermutor(sortOrder).reorder(etmp)
|
||||
episodes.clear()
|
||||
episodes.addAll(etmp)
|
||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
loadItemsRunning = false
|
||||
}
|
||||
}
|
||||
@ -217,8 +219,9 @@ import java.util.concurrent.Semaphore
|
||||
ioScope.cancel()
|
||||
|
||||
feed = null
|
||||
ieMap = mapOf()
|
||||
ueMap = mapOf()
|
||||
episodes.clear()
|
||||
|
||||
tts?.stop()
|
||||
tts?.shutdown()
|
||||
ttsWorking = false
|
||||
@ -308,7 +311,8 @@ import java.util.concurrent.Semaphore
|
||||
while (i < size) {
|
||||
val item = event.episodes[i++]
|
||||
if (item.feedId != feed!!.id) continue
|
||||
// adapter.notifyDataSetChanged()
|
||||
val pos: Int = ieMap[item.id] ?: -1
|
||||
if (pos >= 0) episodes[pos].inQueueState.value = event.inQueue()
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -316,8 +320,9 @@ import java.util.concurrent.Semaphore
|
||||
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
||||
Logd(TAG, "onPlayEvent ${event.episode.title}")
|
||||
if (feed != null) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, event.episode.id)
|
||||
val pos: Int = ieMap[event.episode.id] ?: -1
|
||||
if (pos >= 0) {
|
||||
if (!filterOutEpisode(event.episode)) episodes[pos].isPlayingState.value = event.isPlaying()
|
||||
// if (filterOutEpisode(event.episode)) adapter.updateItems(episodes)
|
||||
// else adapter.notifyItemChangedCompat(pos)
|
||||
}
|
||||
@ -328,10 +333,12 @@ import java.util.concurrent.Semaphore
|
||||
// Logd(TAG, "onEpisodeDownloadEvent() called with ${event.TAG}")
|
||||
if (loadItemsRunning) return
|
||||
if (feed == null || episodes.isEmpty()) return
|
||||
for (downloadUrl in event.urls) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl)
|
||||
for (url in event.urls) {
|
||||
// if (!event.isCompleted(url)) continue
|
||||
val pos: Int = ueMap[url] ?: -1
|
||||
if (pos >= 0) {
|
||||
Logd(TAG, "onEpisodeDownloadEvent $pos ${episodes[pos].title}")
|
||||
Logd(TAG, "onEpisodeDownloadEvent $pos ${event.map[url]?.state} ${episodes[pos].media?.downloaded} ${episodes[pos].title}")
|
||||
episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
// adapter.notifyItemChangedCompat(pos)
|
||||
}
|
||||
}
|
||||
@ -464,6 +471,8 @@ import java.util.concurrent.Semaphore
|
||||
private fun filterOutEpisode(episode: Episode): Boolean {
|
||||
if (enableFilter && !feed?.preferences?.filterString.isNullOrEmpty() && !feed!!.episodeFilter.matches(episode)) {
|
||||
episodes.remove(episode)
|
||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -474,6 +483,8 @@ import java.util.concurrent.Semaphore
|
||||
val episodes_ = list ?: episodes.toList()
|
||||
episodes.clear()
|
||||
episodes.addAll(episodes_.filter { feed!!.episodeFilter.matches(it) })
|
||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,6 +507,8 @@ import java.util.concurrent.Semaphore
|
||||
if (sortOrder != null) getPermutor(sortOrder).reorder(etmp)
|
||||
episodes.clear()
|
||||
episodes.addAll(etmp)
|
||||
ieMap = episodes.withIndex().associate { (index, episode) -> episode.id to index }
|
||||
ueMap = episodes.mapIndexedNotNull { index, episode -> episode.media?.downloadUrl?.let { it to index } }.toMap()
|
||||
if (onInit) {
|
||||
var hasNonMediaItems = false
|
||||
for (item in episodes) {
|
||||
|
@ -827,7 +827,6 @@ class OnlineFeedFragment : Fragment() {
|
||||
toolbar.inflateMenu(R.menu.episodes)
|
||||
toolbar.setTitle(R.string.episodes_label)
|
||||
updateToolbar()
|
||||
// adapter.setOnSelectModeListener(null)
|
||||
return root
|
||||
}
|
||||
override fun onStart() {
|
||||
|
@ -2,8 +2,8 @@ package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.CheckboxDoNotShowAgainBinding
|
||||
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||
import ac.mdiq.podcini.databinding.QueueFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||
@ -13,7 +13,6 @@ import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.database.Queues.clearQueue
|
||||
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
|
||||
import ac.mdiq.podcini.storage.database.Queues.isQueueLocked
|
||||
import ac.mdiq.podcini.storage.database.Queues.moveInQueue
|
||||
import ac.mdiq.podcini.storage.database.Queues.queueKeepSortedOrder
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
@ -26,21 +25,18 @@ import ac.mdiq.podcini.storage.model.PlayQueue
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.compose.InforBar
|
||||
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
|
||||
import ac.mdiq.podcini.ui.view.EpisodeViewHolder
|
||||
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@ -54,7 +50,10 @@ import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.Spinner
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
@ -71,34 +70,35 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.MediaBrowser
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Shows all items in the queue.
|
||||
*/
|
||||
@UnstableApi class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener {
|
||||
@UnstableApi class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private var _binding: QueueFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var recyclerView: EpisodesRecyclerView
|
||||
private lateinit var emptyViewHandler: EmptyViewHandler
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var swipeActions: SwipeActions
|
||||
private lateinit var speedDialView: SpeedDialView
|
||||
|
||||
private var infoBarText = mutableStateOf("")
|
||||
private var leftActionState = mutableStateOf<SwipeAction?>(null)
|
||||
private var rightActionState = mutableStateOf<SwipeAction?>(null)
|
||||
|
||||
private lateinit var spinnerLayout: View
|
||||
private lateinit var queueNames: Array<String>
|
||||
@ -108,13 +108,13 @@ import kotlin.math.max
|
||||
private lateinit var queues: List<PlayQueue>
|
||||
|
||||
private var displayUpArrow = false
|
||||
private var queueItems: MutableList<Episode> = mutableListOf()
|
||||
|
||||
private var adapter: QueueRecyclerAdapter? = null
|
||||
private val queueItems = mutableStateListOf<Episode>()
|
||||
|
||||
private var showBin: Boolean = false
|
||||
private var addToQueueActionItem: SpeedDialActionItem? = null
|
||||
|
||||
private var dragDropEnabled: Boolean = !(isQueueKeepSorted || isQueueLocked)
|
||||
|
||||
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -129,11 +129,11 @@ import kotlin.math.max
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
toolbar = binding.toolbar
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
toolbar.setOnLongClickListener {
|
||||
recyclerView.scrollToPosition(5)
|
||||
recyclerView.post { recyclerView.smoothScrollToPosition(0) }
|
||||
false
|
||||
}
|
||||
// toolbar.setOnLongClickListener {
|
||||
// recyclerView.scrollToPosition(5)
|
||||
// recyclerView.post { recyclerView.smoothScrollToPosition(0) }
|
||||
// false
|
||||
// }
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
|
||||
@ -172,63 +172,42 @@ import kotlin.math.max
|
||||
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
|
||||
toolbar.inflateMenu(R.menu.queue)
|
||||
refreshMenuItems()
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
recyclerView = binding.recyclerView
|
||||
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||
if (animator != null && animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||
// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
|
||||
// registerForContextMenu(recyclerView)
|
||||
// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
||||
|
||||
recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
|
||||
registerForContextMenu(recyclerView)
|
||||
recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
||||
|
||||
swipeActions = QueueSwipeActions()
|
||||
lifecycle.addObserver(swipeActions)
|
||||
swipeActions = SwipeActions(this, TAG)
|
||||
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name))
|
||||
swipeActions.attachTo(recyclerView)
|
||||
refreshSwipeTelltale()
|
||||
binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
binding.infobar.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
}
|
||||
}
|
||||
binding.lazyColumn.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = queueItems,
|
||||
leftSwipeCB = { if (leftActionState.value == null) swipeActions.showDialog() else leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
|
||||
rightSwipeCB = { if (rightActionState.value == null) swipeActions.showDialog() else rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())}, )
|
||||
}
|
||||
}
|
||||
|
||||
adapter = QueueRecyclerAdapter()
|
||||
adapter?.setOnSelectModeListener(this)
|
||||
recyclerView.adapter = adapter
|
||||
lifecycle.addObserver(swipeActions)
|
||||
refreshSwipeTelltale()
|
||||
|
||||
emptyViewHandler = EmptyViewHandler(requireContext())
|
||||
emptyViewHandler.attachToRecyclerView(recyclerView)
|
||||
// emptyViewHandler.attachToRecyclerView(recyclerView)
|
||||
emptyViewHandler.setIcon(R.drawable.ic_playlist_play)
|
||||
emptyViewHandler.setTitle(R.string.no_items_header_label)
|
||||
emptyViewHandler.setMessage(R.string.no_items_label)
|
||||
emptyViewHandler.updateAdapter(adapter)
|
||||
// emptyViewHandler.updateAdapter(adapter)
|
||||
|
||||
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
speedDialView = multiSelectDial.fabSD
|
||||
speedDialView.overlayLayout = multiSelectDial.fabSDOverlay
|
||||
speedDialView.inflate(R.menu.episodes_apply_action_speeddial)
|
||||
addToQueueActionItem = speedDialView.removeActionItemById(R.id.add_to_queue_batch)
|
||||
speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener {
|
||||
override fun onMainActionSelected(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun onToggleChanged(open: Boolean) {
|
||||
if (open && adapter!!.selectedCount == 0) {
|
||||
(activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT)
|
||||
speedDialView.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
|
||||
EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(adapter!!.selectedItems)
|
||||
adapter?.endSelectMode()
|
||||
true
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
Logd(TAG, "onStart() called")
|
||||
super.onStart()
|
||||
adapter?.refreshFragPosCallback = ::refreshPosCallback
|
||||
loadCurQueue(true)
|
||||
procFlowEvents()
|
||||
val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
|
||||
@ -238,29 +217,21 @@ import kotlin.math.max
|
||||
mediaBrowser = browserFuture.get()
|
||||
mediaBrowser?.subscribe("CurQueue", null)
|
||||
}, MoreExecutors.directExecutor())
|
||||
// if (queueItems.isNotEmpty()) recyclerView.restoreScrollPosition(TAG)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Logd(TAG, "onStop() called")
|
||||
super.onStop()
|
||||
adapter?.refreshFragPosCallback = null
|
||||
cancelFlowEvents()
|
||||
mediaBrowser?.unsubscribe("CurQueue")
|
||||
mediaBrowser = null
|
||||
MediaBrowser.releaseFuture(browserFuture)
|
||||
val childCount = recyclerView.childCount
|
||||
for (i in 0 until childCount) {
|
||||
val child = recyclerView.getChildAt(i)
|
||||
val viewHolder = recyclerView.getChildViewHolder(child) as? EpisodeViewHolder
|
||||
viewHolder?.stopDBMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Logd(TAG, "onPause() called")
|
||||
super.onPause()
|
||||
recyclerView.saveScrollPosition(TAG)
|
||||
// recyclerView.saveScrollPosition(TAG)
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
@ -307,31 +278,24 @@ import kotlin.math.max
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshPosCallback(pos: Int, episode: Episode) {
|
||||
// Logd(TAG, "Queue refreshPosCallback: $pos ${episode.title}")
|
||||
if (isAdded && activity != null) refreshInfoBar()
|
||||
}
|
||||
// private fun refreshPosCallback(pos: Int, episode: Episode) {
|
||||
//// Logd(TAG, "Queue refreshPosCallback: $pos ${episode.title}")
|
||||
// if (isAdded && activity != null) refreshInfoBar()
|
||||
// }
|
||||
|
||||
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
|
||||
Logd(TAG, "onQueueEvent() called with ${event.action.name}")
|
||||
if (showBin) return
|
||||
if (adapter == null) {
|
||||
loadCurQueue(true)
|
||||
return
|
||||
}
|
||||
when (event.action) {
|
||||
FlowEvent.QueueEvent.Action.ADDED -> {
|
||||
if (event.episodes.isNotEmpty() && !curQueue.contains(event.episodes[0])) {
|
||||
val pos = queueItems.size
|
||||
queueItems.addAll(event.episodes)
|
||||
adapter?.notifyItemRangeInserted(pos, queueItems.size)
|
||||
adapter?.notifyItemRangeChanged(pos, event.episodes.size)
|
||||
}
|
||||
}
|
||||
FlowEvent.QueueEvent.Action.SET_QUEUE, FlowEvent.QueueEvent.Action.SORTED -> {
|
||||
queueItems.clear()
|
||||
queueItems.addAll(event.episodes)
|
||||
adapter?.updateItems(queueItems)
|
||||
// adapter?.updateItems(queueItems)
|
||||
}
|
||||
FlowEvent.QueueEvent.Action.REMOVED, FlowEvent.QueueEvent.Action.IRREVERSIBLE_REMOVED -> {
|
||||
if (event.episodes.isNotEmpty()) {
|
||||
@ -339,15 +303,8 @@ import kotlin.math.max
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
|
||||
if (pos >= 0) {
|
||||
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
|
||||
// val holder = recyclerView.findViewHolderForItemId(e.id) as? EpisodeViewHolder
|
||||
val holder = recyclerView.findViewHolderForLayoutPosition(pos) as? EpisodeViewHolder
|
||||
if (holder != null) {
|
||||
holder.stopDBMonitor()
|
||||
// holder?.unbind()
|
||||
queueItems.removeAt(pos)
|
||||
adapter?.notifyItemRemoved(pos)
|
||||
// adapter?.notifyItemRangeChanged(pos, adapter!!.itemCount - pos)
|
||||
}
|
||||
queueItems[pos].stopMonitoring.value = true
|
||||
queueItems.removeAt(pos)
|
||||
} else {
|
||||
Log.e(TAG, "Trying to remove item non-existent from queue ${e.id} ${e.title}")
|
||||
continue
|
||||
@ -361,28 +318,28 @@ import kotlin.math.max
|
||||
}
|
||||
FlowEvent.QueueEvent.Action.CLEARED -> {
|
||||
queueItems.clear()
|
||||
adapter?.updateItems(queueItems)
|
||||
}
|
||||
FlowEvent.QueueEvent.Action.MOVED, FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return
|
||||
}
|
||||
adapter?.updateDragDropEnabled()
|
||||
// adapter?.updateDragDropEnabled()
|
||||
refreshMenuItems()
|
||||
recyclerView.saveScrollPosition(TAG)
|
||||
// recyclerView.saveScrollPosition(TAG)
|
||||
refreshInfoBar()
|
||||
}
|
||||
|
||||
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id)
|
||||
Logd(TAG, "onPlayEvent action: ${event.action} pos: $pos ${event.episode.title}")
|
||||
if (pos >= 0) adapter?.notifyItemChangedCompat(pos)
|
||||
if (pos >= 0) queueItems[pos].isPlayingState.value = event.isPlaying()
|
||||
}
|
||||
|
||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||
if (loadItemsRunning) return
|
||||
for (downloadUrl in event.urls) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), downloadUrl)
|
||||
if (pos >= 0) adapter?.notifyItemChangedCompat(pos)
|
||||
for (url in event.urls) {
|
||||
// if (!event.isCompleted(url)) continue
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), url)
|
||||
if (pos >= 0) queueItems[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
}
|
||||
}
|
||||
|
||||
@ -410,16 +367,16 @@ import kotlin.math.max
|
||||
}
|
||||
|
||||
private fun refreshSwipeTelltale() {
|
||||
if (swipeActions.actions?.left != null) binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
|
||||
if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
|
||||
leftActionState.value = swipeActions.actions?.left
|
||||
rightActionState.value = swipeActions.actions?.right
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun onKeyUp(event: KeyEvent) {
|
||||
if (!isAdded || !isVisible || !isMenuVisible) return
|
||||
when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_T -> recyclerView.smoothScrollToPosition(0)
|
||||
KeyEvent.KEYCODE_B -> recyclerView.smoothScrollToPosition(adapter!!.itemCount - 1)
|
||||
// KeyEvent.KEYCODE_T -> recyclerView.smoothScrollToPosition(0)
|
||||
// KeyEvent.KEYCODE_B -> recyclerView.smoothScrollToPosition(adapter!!.itemCount - 1)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@ -427,10 +384,7 @@ import kotlin.math.max
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
queueItems = mutableListOf()
|
||||
adapter?.endSelectMode()
|
||||
adapter?.clearData()
|
||||
adapter = null
|
||||
queueItems.clear()
|
||||
toolbar.setOnMenuItemClickListener(null)
|
||||
toolbar.setOnLongClickListener(null)
|
||||
super.onDestroyView()
|
||||
@ -468,10 +422,10 @@ import kotlin.math.max
|
||||
refreshMenuItems()
|
||||
if (showBin) {
|
||||
item.setIcon(R.drawable.playlist_play)
|
||||
speedDialView.addActionItem(addToQueueActionItem)
|
||||
// speedDialView.addActionItem(addToQueueActionItem)
|
||||
} else {
|
||||
item.setIcon(R.drawable.ic_history)
|
||||
speedDialView.removeActionItem(addToQueueActionItem)
|
||||
// speedDialView.removeActionItem(addToQueueActionItem)
|
||||
}
|
||||
loadCurQueue(false)
|
||||
}
|
||||
@ -635,7 +589,7 @@ import kotlin.math.max
|
||||
@UnstableApi private fun setQueueLocked(locked: Boolean) {
|
||||
isQueueLocked = locked
|
||||
refreshMenuItems()
|
||||
adapter?.updateDragDropEnabled()
|
||||
// adapter?.updateDragDropEnabled()
|
||||
|
||||
if (queueItems.size == 0) {
|
||||
if (locked) (activity as MainActivity).showSnackbarAbovePlayer(R.string.queue_locked, Snackbar.LENGTH_SHORT)
|
||||
@ -664,7 +618,7 @@ import kotlin.math.max
|
||||
info += " • "
|
||||
info += DurationConverter.getDurationStringLocalized(requireActivity(), timeLeft)
|
||||
}
|
||||
binding.infoBar.text = info
|
||||
infoBarText.value = info
|
||||
// toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}"
|
||||
}
|
||||
|
||||
@ -685,30 +639,13 @@ import kotlin.math.max
|
||||
}
|
||||
Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}")
|
||||
|
||||
binding.progressBar.visibility = View.GONE
|
||||
// adapter?.setDummyViews(0)
|
||||
adapter?.updateItems(queueItems)
|
||||
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
||||
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
||||
refreshInfoBar()
|
||||
// playbackService?.notifyCurQueueItemsChanged()
|
||||
loadItemsRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartSelectMode() {
|
||||
swipeActions.detach()
|
||||
speedDialView.visibility = View.VISIBLE
|
||||
refreshMenuItems()
|
||||
binding.infoBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onEndSelectMode() {
|
||||
speedDialView.close()
|
||||
speedDialView.visibility = View.GONE
|
||||
binding.infoBar.visibility = View.VISIBLE
|
||||
swipeActions.attachTo(recyclerView)
|
||||
}
|
||||
|
||||
class QueueSortDialog : EpisodeSortDialog() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
if (isQueueKeepSorted) sortOrder = queueKeepSortedOrder
|
||||
@ -757,88 +694,6 @@ import kotlin.math.max
|
||||
}
|
||||
}
|
||||
|
||||
private inner class QueueSwipeActions : SwipeActions(ItemTouchHelper.UP or ItemTouchHelper.DOWN, this@QueuesFragment, TAG) {
|
||||
// Position tracking whilst dragging
|
||||
var dragFrom: Int = -1
|
||||
var dragTo: Int = -1
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
val fromPosition = viewHolder.bindingAdapterPosition
|
||||
val toPosition = target.bindingAdapterPosition
|
||||
|
||||
// Update tracked position
|
||||
if (dragFrom == -1) dragFrom = fromPosition
|
||||
dragTo = toPosition
|
||||
|
||||
val from = viewHolder.bindingAdapterPosition
|
||||
val to = target.bindingAdapterPosition
|
||||
Logd(TAG, "move($from, $to) in memory")
|
||||
if (from >= queueItems.size || to >= queueItems.size || from < 0 || to < 0) return false
|
||||
|
||||
queueItems.add(to, queueItems.removeAt(from))
|
||||
adapter?.notifyItemMoved(from, to)
|
||||
return true
|
||||
}
|
||||
@UnstableApi override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
//SwipeActions
|
||||
super.onSwiped(viewHolder, direction)
|
||||
}
|
||||
override fun isLongPressDragEnabled(): Boolean {
|
||||
return false
|
||||
}
|
||||
@UnstableApi override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
// Check if drag finished
|
||||
if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) reallyMoved(dragFrom, dragTo)
|
||||
dragTo = -1
|
||||
dragFrom = dragTo
|
||||
}
|
||||
@UnstableApi private fun reallyMoved(from: Int, to: Int) {
|
||||
Logd(TAG, "Write to database move($from, $to)")
|
||||
moveInQueue(from, to, true)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class QueueRecyclerAdapter : EpisodesAdapter(activity as MainActivity) {
|
||||
private var dragDropEnabled: Boolean
|
||||
|
||||
init {
|
||||
dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
|
||||
}
|
||||
fun updateDragDropEnabled() {
|
||||
dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@UnstableApi
|
||||
override fun afterBindViewHolder(holder: EpisodeViewHolder, pos: Int) {
|
||||
if (inActionMode() || !dragDropEnabled) {
|
||||
holder.dragHandle.visibility = View.GONE
|
||||
holder.dragHandle.setOnTouchListener(null)
|
||||
// holder.coverHolder.setOnTouchListener(null)
|
||||
} else {
|
||||
holder.dragHandle.visibility = View.VISIBLE
|
||||
holder.dragHandle.setOnTouchListener { _: View?, event: MotionEvent ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) swipeActions.startDrag(holder)
|
||||
false
|
||||
}
|
||||
holder.coverHolder.setOnTouchListener { v1, event ->
|
||||
if (!inActionMode() && event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
val isLtr = holder.itemView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR
|
||||
val factor = (if (isLtr) 1 else -1).toFloat()
|
||||
if (factor * event.x < factor * 0.5 * v1.width) swipeActions.startDrag(holder)
|
||||
else Logd(TAG, "Ignoring drag in right half of the image")
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
if (inActionMode()) {
|
||||
holder.dragHandle.setOnTouchListener(null)
|
||||
// holder.coverHolder.setOnTouchListener(null)
|
||||
}
|
||||
holder.isInQueue.setVisibility(View.GONE)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = QueuesFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
|
@ -3,25 +3,21 @@ package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.HorizontalFeedItemBinding
|
||||
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||
import ac.mdiq.podcini.databinding.SearchFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler
|
||||
import ac.mdiq.podcini.ui.actions.handler.MenuItemUtils
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.dialog.CustomFeedNameDialog
|
||||
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
||||
import ac.mdiq.podcini.ui.dialog.TagSettingsDialog
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
|
||||
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||
import ac.mdiq.podcini.ui.view.SquareImageView
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
@ -37,10 +33,10 @@ import android.util.Pair
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@ -51,9 +47,6 @@ import coil.load
|
||||
import coil.request.ImageRequest
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@ -65,21 +58,17 @@ import java.lang.ref.WeakReference
|
||||
* Performs a search operation on all feeds or one specific feed and displays the search result.
|
||||
*/
|
||||
@UnstableApi
|
||||
class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
class SearchFragment : Fragment() {
|
||||
private var _binding: SearchFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var adapter: EpisodesAdapter
|
||||
private lateinit var adapterFeeds: HorizontalFeedListAdapter
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var emptyViewHandler: EmptyViewHandler
|
||||
private lateinit var recyclerView: EpisodesRecyclerView
|
||||
private lateinit var searchView: SearchView
|
||||
private lateinit var sdBinding: MultiSelectSpeedDialBinding
|
||||
private lateinit var chip: Chip
|
||||
private lateinit var automaticSearchDebouncer: Handler
|
||||
|
||||
private var results: MutableList<Episode> = mutableListOf()
|
||||
private val results = mutableStateListOf<Episode>()
|
||||
|
||||
private var lastQueryChange: Long = 0
|
||||
private var isOtherViewInFoucus = false
|
||||
@ -92,18 +81,16 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = SearchFragmentBinding.inflate(inflater)
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
setupToolbar(binding.toolbar)
|
||||
sdBinding = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
progressBar = binding.progressBar
|
||||
recyclerView = binding.recyclerView
|
||||
recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
|
||||
registerForContextMenu(recyclerView)
|
||||
adapter = object : EpisodesAdapter(activity as MainActivity) {}
|
||||
adapter.setOnSelectModeListener(this)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
||||
|
||||
binding.lazyColumn.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = results,
|
||||
leftSwipeCB = {},
|
||||
rightSwipeCB = { })
|
||||
}
|
||||
}
|
||||
|
||||
val recyclerViewFeeds = binding.recyclerViewFeeds
|
||||
val layoutManagerFeeds = LinearLayoutManager(activity)
|
||||
@ -118,7 +105,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
recyclerViewFeeds.adapter = adapterFeeds
|
||||
|
||||
emptyViewHandler = EmptyViewHandler(requireContext())
|
||||
emptyViewHandler.attachToRecyclerView(recyclerView)
|
||||
// emptyViewHandler.attachToRecyclerView(recyclerView)
|
||||
emptyViewHandler.setIcon(R.drawable.ic_search)
|
||||
emptyViewHandler.setTitle(R.string.search_status_no_results)
|
||||
emptyViewHandler.setMessage(R.string.type_to_search)
|
||||
@ -135,35 +122,6 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
searchView.setOnQueryTextFocusChangeListener { view: View, hasFocus: Boolean ->
|
||||
if (hasFocus && !isOtherViewInFoucus) showInputMethod(view.findFocus())
|
||||
}
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
|
||||
val imm = activity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(recyclerView.windowToken, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
sdBinding.fabSD.overlayLayout = sdBinding.fabSDOverlay
|
||||
sdBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial)
|
||||
sdBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener {
|
||||
override fun onMainActionSelected(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun onToggleChanged(open: Boolean) {
|
||||
if (open && adapter.selectedCount == 0) {
|
||||
(activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT)
|
||||
sdBinding.fabSD.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
sdBinding.fabSD.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
|
||||
EpisodeMultiSelectHandler(activity as MainActivity, actionItem.id)
|
||||
.handleAction(adapter.selectedItems)
|
||||
adapter.endSelectMode()
|
||||
true
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ -180,8 +138,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
adapter.clearData()
|
||||
results = mutableListOf()
|
||||
results.clear()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@ -204,12 +161,10 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
}
|
||||
@UnstableApi override fun onQueryTextChange(s: String): Boolean {
|
||||
automaticSearchDebouncer.removeCallbacksAndMessages(null)
|
||||
if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0L && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL)) {
|
||||
if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0L && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL))
|
||||
search()
|
||||
} else {
|
||||
// Don't search instantly with first symbol after some pause
|
||||
automaticSearchDebouncer.postDelayed({ search(); lastQueryChange = 0 }, (SEARCH_DEBOUNCE_INTERVAL / 2).toLong())
|
||||
}
|
||||
// Don't search instantly with first symbol after some pause
|
||||
else automaticSearchDebouncer.postDelayed({ search(); lastQueryChange = 0 }, (SEARCH_DEBOUNCE_INTERVAL / 2).toLong())
|
||||
lastQueryChange = System.currentTimeMillis()
|
||||
return false
|
||||
}
|
||||
@ -228,12 +183,6 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
val selectedFeedItem: Feed? = adapterFeeds.longPressedItem
|
||||
if (selectedFeedItem != null && onMenuItemClicked(this, item.itemId, selectedFeedItem) {}) return true
|
||||
|
||||
val selectedItem: Episode? = adapter.longPressedItem
|
||||
if (selectedItem != null) {
|
||||
// if (adapter.onContextItemSelected(item)) return true
|
||||
if (EpisodeMenuHandler.onMenuItemClicked(this, item.itemId, selectedItem)) return true
|
||||
}
|
||||
return super.onContextItemSelected(item)
|
||||
}
|
||||
|
||||
@ -280,14 +229,14 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
}
|
||||
|
||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
for (downloadUrl in event.urls) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(results, downloadUrl)
|
||||
if (pos >= 0) adapter.notifyItemChangedCompat(pos)
|
||||
for (url in event.urls) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(results, url)
|
||||
if (pos >= 0) results[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun searchWithProgressBar() {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
emptyViewHandler.hide()
|
||||
search()
|
||||
}
|
||||
@ -299,23 +248,20 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val results = withContext(Dispatchers.IO) { performSearch() }
|
||||
val results_ = withContext(Dispatchers.IO) { performSearch() }
|
||||
withContext(Dispatchers.Main) {
|
||||
progressBar.visibility = View.GONE
|
||||
if (results.first != null) {
|
||||
val first_ = results.first!!.toMutableList()
|
||||
this@SearchFragment.results = first_
|
||||
adapter.updateItems(first_)
|
||||
if (results_.first != null) {
|
||||
val first_ = results_.first!!.toMutableList()
|
||||
results.clear()
|
||||
results.addAll(first_)
|
||||
}
|
||||
if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
|
||||
if (results.second != null) adapterFeeds.updateData(results.second!!)
|
||||
if (results_.second != null) adapterFeeds.updateData(results_.second!!)
|
||||
} else adapterFeeds.updateData(emptyList())
|
||||
if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search)
|
||||
else emptyViewHandler.setMessage(getString(R.string.no_results_for_query, searchView.query))
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,29 +348,6 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
(activity as MainActivity).loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
}
|
||||
|
||||
override fun onStartSelectMode() {
|
||||
searchViewFocusOff()
|
||||
sdBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch)
|
||||
sdBinding.fabSD.removeActionItemById(R.id.delete_batch)
|
||||
sdBinding.fabSD.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onEndSelectMode() {
|
||||
sdBinding.fabSD.close()
|
||||
sdBinding.fabSD.visibility = View.GONE
|
||||
searchViewFocusOn()
|
||||
}
|
||||
|
||||
private fun searchViewFocusOff() {
|
||||
isOtherViewInFoucus = true
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
private fun searchViewFocusOn() {
|
||||
isOtherViewInFoucus = false
|
||||
searchView.requestFocus()
|
||||
}
|
||||
|
||||
open class HorizontalFeedListAdapter(mainActivity: MainActivity)
|
||||
: RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
|
||||
|
||||
@ -436,9 +359,6 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
private var endButtonText = 0
|
||||
private var endButtonAction: Runnable? = null
|
||||
|
||||
// fun setDummyViews(dummyViews: Int) {
|
||||
// this.dummyViews = dummyViews
|
||||
// }
|
||||
fun updateData(newData: List<Feed>?) {
|
||||
data.clear()
|
||||
data.addAll(newData!!)
|
||||
@ -533,18 +453,12 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||
return fragment
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SearchFragment that searches all feeds with pre-defined query.
|
||||
*/
|
||||
fun newInstance(query: String?): SearchFragment {
|
||||
val fragment = newInstance()
|
||||
fragment.requireArguments().putString(ARG_QUERY, query)
|
||||
return fragment
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SearchFragment that searches one specific feed.
|
||||
*/
|
||||
fun newInstance(feedId: Long, feedTitle: String?): SearchFragment {
|
||||
val fragment = newInstance()
|
||||
fragment.requireArguments().putLong(ARG_FEED, feedId)
|
||||
|
@ -15,6 +15,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
|
||||
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
@ -48,6 +49,8 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@ -57,8 +60,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.util.Consumer
|
||||
@ -67,6 +72,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
@ -208,7 +214,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
// }
|
||||
|
||||
// binding.progressBar.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
|
||||
val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd
|
||||
subscriptionAddButton.setOnClickListener {
|
||||
@ -571,6 +577,36 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||
adapter.setItems(feedsOnly)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedExpanded(feed: Feed) {
|
||||
Row {
|
||||
val imgLoc = ""
|
||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover",
|
||||
placeholder = painterResource(R.mipmap.ic_launcher),
|
||||
modifier = Modifier.width(80.dp).height(80.dp)
|
||||
.clickable(onClick = {
|
||||
Logd(TAG, "icon clicked!")
|
||||
// if (selectMode) toggleSelected()
|
||||
// else activity.loadChildFragment(FeedInfoFragment.newInstance(episode.feed!!))
|
||||
}))
|
||||
val textColor = MaterialTheme.colors.onSurface
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text("titleLabel", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text("producerLabel", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Row {
|
||||
Text("episodeCount", color = textColor, )
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("sortInfo", color = textColor, )
|
||||
}
|
||||
}
|
||||
Icon(painter = painterResource(R.drawable.ic_error), contentDescription = "error")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeedCompact(feed: Feed) {
|
||||
|
||||
}
|
||||
@UnstableApi
|
||||
private inner class FeedMultiSelectActionHandler(private val activity: MainActivity, private val selectedItems: List<Feed>) {
|
||||
fun handleAction(id: Int) {
|
||||
|
@ -1,327 +0,0 @@
|
||||
package ac.mdiq.podcini.ui.view
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FeeditemlistItemBinding
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Episode.PlayState
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Feed.Companion.PREFIX_GENERATIVE_COVER
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.EpisodeActionButton
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.TTSActionButton
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.utils.CoverLoader
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatForAccessibility
|
||||
import android.text.Layout
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import io.realm.kotlin.notifications.SingleQueryChange
|
||||
import io.realm.kotlin.notifications.UpdatedObject
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Holds the view which shows FeedItems.
|
||||
*/
|
||||
@UnstableApi
|
||||
open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGroup, var refreshAdapterPosCallback: ((Int, Episode) -> Unit)? = null)
|
||||
: RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.feeditemlist_item, parent, false)) {
|
||||
|
||||
val binding: FeeditemlistItemBinding = FeeditemlistItemBinding.bind(itemView)
|
||||
|
||||
private val placeholder: TextView = binding.txtvPlaceholder
|
||||
private val cover: ImageView = binding.imgvCover
|
||||
private val title: TextView = binding.txtvTitle
|
||||
private val position: TextView = binding.txtvPosition
|
||||
private val duration: TextView = binding.txtvDuration
|
||||
private val isVideo: ImageView = binding.ivIsVideo
|
||||
private val progressBar: ProgressBar = binding.progressBar
|
||||
|
||||
private var posIndex: Int = -1
|
||||
|
||||
private var actionButton: EpisodeActionButton? = null
|
||||
private val secondaryActionProgress: CircularProgressBar = binding.secondaryActionButton.secondaryActionProgress
|
||||
|
||||
protected val pubDate: TextView = binding.txtvPubDate
|
||||
|
||||
@JvmField
|
||||
val dragHandle: ImageView = binding.dragHandle
|
||||
|
||||
@JvmField
|
||||
val isInQueue: ImageView = binding.ivInPlaylist
|
||||
|
||||
@JvmField
|
||||
val secondaryActionButton: View = binding.secondaryActionButton.root
|
||||
@JvmField
|
||||
val secondaryActionIcon: ImageView = binding.secondaryActionButton.secondaryActionIcon
|
||||
|
||||
@JvmField
|
||||
val coverHolder: CardView = binding.coverHolder
|
||||
@JvmField
|
||||
val infoCard: LinearLayout = binding.infoCard
|
||||
|
||||
var episode: Episode? = null
|
||||
|
||||
private var episodeMonitor: Job? = null
|
||||
private var mediaMonitor: Job? = null
|
||||
private var notBond: Boolean = true
|
||||
|
||||
private val isCurMedia: Boolean
|
||||
get() = InTheatre.isCurMedia(this.episode?.media)
|
||||
|
||||
init {
|
||||
title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(item: Episode) {
|
||||
if (episodeMonitor == null) {
|
||||
val item_ = realm.query(Episode::class).query("id == ${item.id}").first()
|
||||
episodeMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||
val episodeFlow = item_.asFlow()
|
||||
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
||||
when (changes) {
|
||||
is UpdatedObject -> {
|
||||
Logd(TAG, "episodeMonitor UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||
withContext(Dispatchers.Main) {
|
||||
bind(changes.obj)
|
||||
if (posIndex >= 0) refreshAdapterPosCallback?.invoke(posIndex, changes.obj)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
// return
|
||||
}
|
||||
}
|
||||
if (mediaMonitor == null) {
|
||||
val item_ = realm.query(Episode::class).query("id == ${item.id}").first()
|
||||
mediaMonitor = CoroutineScope(Dispatchers.Default).launch {
|
||||
val episodeFlow = item_.asFlow(listOf("media.*"))
|
||||
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
|
||||
when (changes) {
|
||||
is UpdatedObject -> {
|
||||
Logd(TAG, "mediaMonitor UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||
withContext(Dispatchers.Main) {
|
||||
updatePlaybackPositionNew(changes.obj)
|
||||
// bind(changes.obj)
|
||||
if (posIndex >= 0) refreshAdapterPosCallback?.invoke(posIndex, changes.obj)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.episode = item
|
||||
placeholder.text = item.feed?.title
|
||||
title.text = item.title
|
||||
binding.container.alpha = if (item.isPlayed()) 0.7f else 1.0f
|
||||
binding.leftPadding.contentDescription = item.title
|
||||
binding.playedMark.visibility = View.GONE
|
||||
binding.txtvPubDate.setTextColor(getColorFromAttr(activity, com.google.android.material.R.attr.colorOnSurfaceVariant))
|
||||
when {
|
||||
item.isPlayed() -> {
|
||||
binding.leftPadding.contentDescription = item.title + ". " + activity.getString(R.string.is_played)
|
||||
binding.playedMark.visibility = View.VISIBLE
|
||||
binding.playedMark.alpha = 1.0f
|
||||
}
|
||||
item.isNew -> {
|
||||
binding.txtvPubDate.setTextColor(getColorFromAttr(activity, androidx.appcompat.R.attr.colorAccent))
|
||||
}
|
||||
}
|
||||
|
||||
setPubDate(item)
|
||||
|
||||
binding.isFavorite.visibility = if (item.isFavorite) View.VISIBLE else View.GONE
|
||||
isInQueue.visibility = if (curQueue.contains(item)) View.VISIBLE else View.GONE
|
||||
// container.alpha = if (item.isPlayed()) 0.7f else 1.0f
|
||||
|
||||
val newButton = EpisodeActionButton.forItem(item)
|
||||
// Logd(TAG, "Trying to bind button ${actionButton?.TAG} ${newButton.TAG} ${item.title}")
|
||||
// not using a new button to ensure valid progress values, for TTS audio generation
|
||||
if (!(actionButton?.TAG == TTSActionButton::class.simpleName && newButton.TAG == TTSActionButton::class.simpleName)) {
|
||||
actionButton = newButton
|
||||
actionButton?.configure(secondaryActionButton, secondaryActionIcon, activity)
|
||||
secondaryActionButton.isFocusable = false
|
||||
}
|
||||
|
||||
// Log.d(TAG, "bind called ${item.media}")
|
||||
when {
|
||||
item.media != null -> bind(item.media!!)
|
||||
// for generating TTS files for episode without media
|
||||
item.playState == PlayState.BUILDING.code -> {
|
||||
secondaryActionProgress.setPercentage(actionButton!!.processing, item)
|
||||
secondaryActionProgress.setIndeterminate(false)
|
||||
}
|
||||
else -> {
|
||||
secondaryActionProgress.setPercentage(0f, item)
|
||||
secondaryActionProgress.setIndeterminate(false)
|
||||
isVideo.visibility = View.GONE
|
||||
progressBar.visibility = View.GONE
|
||||
duration.visibility = View.GONE
|
||||
position.visibility = View.GONE
|
||||
itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, androidx.appcompat.R.attr.selectableItemBackground))
|
||||
}
|
||||
}
|
||||
|
||||
if (notBond && coverHolder.visibility == View.VISIBLE) {
|
||||
cover.setImageDrawable(null)
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(item)
|
||||
// Logd(TAG, "imgLoc $imgLoc ${item.feed?.imageUrl} ${item.title}")
|
||||
if (!imgLoc.isNullOrBlank() && !imgLoc.contains(PREFIX_GENERATIVE_COVER))
|
||||
CoverLoader(activity)
|
||||
.withUri(imgLoc)
|
||||
.withFallbackUri(item.feed?.imageUrl)
|
||||
.withPlaceholderView(placeholder)
|
||||
.withCoverView(cover)
|
||||
.load()
|
||||
else {
|
||||
Logd(TAG, "setting cover to ic_launcher")
|
||||
cover.setImageDrawable(AppCompatResources.getDrawable(activity, R.drawable.ic_launcher_foreground))
|
||||
}
|
||||
// if (item.isNew) cover.setColorFilter(ContextCompat.getColor(activity, R.color.gradient_100), PorterDuff.Mode.MULTIPLY)
|
||||
}
|
||||
notBond = false
|
||||
}
|
||||
|
||||
internal fun setPosIndex(index: Int) {
|
||||
posIndex = index
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
Logd(TAG, "unbind ${title.text}")
|
||||
// Cancel coroutine here
|
||||
itemView.setOnClickListener(null)
|
||||
itemView.setOnCreateContextMenuListener(null)
|
||||
itemView.setOnLongClickListener(null)
|
||||
itemView.setOnTouchListener(null)
|
||||
secondaryActionButton.setOnClickListener(null)
|
||||
dragHandle.setOnTouchListener(null)
|
||||
coverHolder.setOnTouchListener(null)
|
||||
posIndex = -1
|
||||
episode = null
|
||||
notBond = true
|
||||
stopDBMonitor()
|
||||
}
|
||||
|
||||
fun stopDBMonitor() {
|
||||
episodeMonitor?.cancel()
|
||||
episodeMonitor = null
|
||||
mediaMonitor?.cancel()
|
||||
mediaMonitor = null
|
||||
}
|
||||
|
||||
open fun setPubDate(item: Episode) {
|
||||
pubDate.text = formatAbbrev(activity, item.getPubDate())
|
||||
pubDate.setContentDescription(formatForAccessibility(item.getPubDate()))
|
||||
}
|
||||
|
||||
private fun bind(media: EpisodeMedia) {
|
||||
isVideo.visibility = if (media.getMediaType() == MediaType.VIDEO) View.VISIBLE else View.GONE
|
||||
duration.visibility = if (media.getDuration() > 0) View.VISIBLE else View.GONE
|
||||
|
||||
if (isCurMedia) {
|
||||
val density: Float = activity.resources.displayMetrics.density
|
||||
itemView.setBackgroundColor(SurfaceColors.getColorForElevation(activity, 8 * density))
|
||||
} else itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, androidx.appcompat.R.attr.selectableItemBackground))
|
||||
|
||||
val dls = DownloadServiceInterface.get()
|
||||
when {
|
||||
media.downloadUrl != null && dls?.isDownloadingEpisode(media.downloadUrl!!) == true -> {
|
||||
val percent: Float = 0.01f * dls.getProgress(media.downloadUrl!!)
|
||||
secondaryActionProgress.setPercentage(max(percent, 0.01f), this.episode)
|
||||
secondaryActionProgress.setIndeterminate(dls.isEpisodeQueued(media.downloadUrl!!))
|
||||
}
|
||||
media.downloaded -> {
|
||||
secondaryActionProgress.setPercentage(1f, this.episode) // Do not animate 100% -> 0%
|
||||
secondaryActionProgress.setIndeterminate(false)
|
||||
}
|
||||
else -> {
|
||||
secondaryActionProgress.setPercentage(0f, this.episode) // Animate X% -> 0%
|
||||
secondaryActionProgress.setIndeterminate(false)
|
||||
}
|
||||
}
|
||||
|
||||
duration.text = DurationConverter.getDurationStringLong(media.getDuration())
|
||||
duration.setContentDescription(activity.getString(R.string.chapter_duration,
|
||||
DurationConverter.getDurationStringLocalized(activity, media.getDuration().toLong())))
|
||||
if (isCurMedia || this.episode?.isInProgress == true) {
|
||||
val progress: Int = (100.0 * media.getPosition() / media.getDuration()).toInt()
|
||||
val remainingTime = max((media.getDuration() - media.getPosition()).toDouble(), 0.0).toInt()
|
||||
progressBar.progress = progress
|
||||
position.text = DurationConverter.getDurationStringLong(media.getPosition())
|
||||
position.setContentDescription(activity.getString(R.string.position,
|
||||
DurationConverter.getDurationStringLocalized(activity, media.getPosition().toLong())))
|
||||
progressBar.visibility = View.VISIBLE
|
||||
position.visibility = View.VISIBLE
|
||||
if (UserPreferences.shouldShowRemainingTime()) {
|
||||
duration.text = (if ((remainingTime > 0)) "-" else "") + DurationConverter.getDurationStringLong(remainingTime)
|
||||
duration.setContentDescription(activity.getString(R.string.chapter_duration,
|
||||
DurationConverter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition()).toLong())))
|
||||
}
|
||||
} else {
|
||||
progressBar.visibility = View.GONE
|
||||
position.visibility = View.GONE
|
||||
}
|
||||
|
||||
when {
|
||||
media.size > 0 -> binding.size.text = Formatter.formatShortFileSize(activity, media.size)
|
||||
else -> binding.size.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePlaybackPositionNew(item: Episode) {
|
||||
Logd(TAG, "updatePlaybackPositionNew called")
|
||||
this.episode = item
|
||||
val currentPosition = item.media?.position ?: 0
|
||||
val timeDuration = item.media?.duration ?: 0
|
||||
progressBar.progress = (100.0 * currentPosition / timeDuration).toInt()
|
||||
position.text = DurationConverter.getDurationStringLong(currentPosition)
|
||||
|
||||
val remainingTime = max((timeDuration - currentPosition).toDouble(), 0.0).toInt()
|
||||
if (currentPosition == Playable.INVALID_TIME || timeDuration == Playable.INVALID_TIME) {
|
||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||
return
|
||||
}
|
||||
if (UserPreferences.shouldShowRemainingTime()) duration.text = (if (remainingTime > 0) "-" else "") + DurationConverter.getDurationStringLong(remainingTime)
|
||||
else duration.text = DurationConverter.getDurationStringLong(timeDuration)
|
||||
duration.visibility = View.VISIBLE // Even if the duration was previously unknown, it is now known
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the separator dot between icons and text if there are no icons.
|
||||
*/
|
||||
fun hideSeparatorIfNecessary() {
|
||||
val hasIcons = isInQueue.visibility == View.VISIBLE || isVideo.visibility == View.VISIBLE || binding.isFavorite.visibility == View.VISIBLE
|
||||
binding.separatorIcons.visibility = if (hasIcons) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = EpisodeViewHolder::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
}
|
@ -29,6 +29,9 @@ sealed class FlowEvent {
|
||||
|
||||
data class PlayEvent(val episode: Episode, val action: Action = Action.START) : FlowEvent() {
|
||||
enum class Action { START, END, }
|
||||
fun isPlaying(): Boolean {
|
||||
return action == Action.START
|
||||
}
|
||||
}
|
||||
|
||||
data class PlayerErrorEvent(val message: String) : FlowEvent()
|
||||
@ -63,6 +66,13 @@ sealed class FlowEvent {
|
||||
enum class Action {
|
||||
ADDED, SET_QUEUE, REMOVED, IRREVERSIBLE_REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED, SWITCH_QUEUE
|
||||
}
|
||||
fun inQueue(): Boolean {
|
||||
return when (action) {
|
||||
Action.ADDED, Action.SET_QUEUE, Action.SORTED, Action.MOVED, Action.SWITCH_QUEUE -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
}
|
||||
companion object {
|
||||
fun added(episode: Episode, position: Int): QueueEvent {
|
||||
return QueueEvent(Action.ADDED, listOf(episode), position)
|
||||
@ -149,6 +159,10 @@ sealed class FlowEvent {
|
||||
data class EpisodeDownloadEvent(val map: Map<String, DownloadStatus>) : FlowEvent() {
|
||||
val urls: Set<String>
|
||||
get() = map.keys
|
||||
fun isCompleted(url: String): Boolean {
|
||||
val stat = map[url]
|
||||
return stat?.state == DownloadStatus.State.COMPLETED.ordinal
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: need better handling at receving end
|
||||
|
@ -1,88 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/audioplayer_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/playerFragment1"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/composeView1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/external_player_height"
|
||||
android:layout_gravity="top"
|
||||
android:elevation="8dp"
|
||||
android:outlineProvider="none"
|
||||
tools:layout_height="@dimen/external_player_height" />
|
||||
android:outlineProvider="none"/>
|
||||
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:minHeight="?android:attr/actionBarSize"
|
||||
android:theme="?attr/actionBarTheme"
|
||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="@drawable/ic_arrow_down" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/itemDescription"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/composeDetailView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_above="@id/playerFragment2"
|
||||
android:layout_below="@id/toolbar"
|
||||
android:layout_marginBottom="12dp" />
|
||||
android:layout_weight="1"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<ImageView
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/composeView2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/bg_gradient"
|
||||
app:tint="?android:attr/colorBackground"/>
|
||||
android:layout_height="@dimen/external_player_height"/>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardViewSeek"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@+id/itemDescription"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:alpha="0"
|
||||
app:cardBackgroundColor="?attr/seek_background"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="0dp"
|
||||
tools:alpha="1">
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvSeek"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="1:06:29" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/playerFragment2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/external_player_height"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_alignParentBottom="true"
|
||||
tools:layout_height="@dimen/external_player_height" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
@ -3,6 +3,7 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/episode_info_fragment"
|
||||
@ -18,226 +19,9 @@
|
||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/feeditem_fragment"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/composeView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgvCover"
|
||||
android:layout_width="@dimen/thumbnail_length_queue_item"
|
||||
android:layout_height="@dimen/thumbnail_length_queue_item"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="@string/open_podcast"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvPodcast"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:importantForAccessibility="no"
|
||||
style="@style/Podcini.TextView.ListItemSecondaryTitle"
|
||||
tools:text="Podcast title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
tools:text="@sample/episodes.json/data/title" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvPublished"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Podcini.TextView.ListItemSecondaryTitle"
|
||||
tools:text="Jan 23" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:text="·"
|
||||
style="@style/Podcini.TextView.ListItemSecondaryTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvDuration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Podcini.TextView.ListItemSecondaryTitle"
|
||||
tools:text="@sample/episodes.json/data/duration" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:text="·"
|
||||
style="@style/Podcini.TextView.ListItemSecondaryTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvSize"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Podcini.TextView.ListItemSecondaryTitle"
|
||||
tools:text="@sample/episodes.json/data/duration" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/butAction1"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
tools:src="@drawable/ic_settings" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/homeButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginLeft="40dp"
|
||||
android:layout_marginStart="40dp"
|
||||
android:layout_marginRight="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:src="@drawable/baseline_home_work_24"
|
||||
tools:src="@drawable/ic_settings" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.CircularProgressBar
|
||||
android:id="@+id/circularProgressBar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
app:foregroundColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/butAction2"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
tools:src="@drawable/ic_settings" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/noMediaLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:textAlignment="center"
|
||||
android:background="?android:attr/dividerVertical"
|
||||
android:text="@string/no_media_label" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/dividerVertical" />
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/header"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
android:id="@+id/webvDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/item_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
style="@style/Podcini.TextView.ListItemBody"
|
||||
tools:text="Item link" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progbarLoading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
@ -1,227 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/headerContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="@dimen/additional_horizontal_spacing"
|
||||
android:layout_marginTop="?android:attr/actionBarSize"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="130dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="bottom">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="2dp"
|
||||
android:background="@color/image_readability_tint"
|
||||
android:orientation="horizontal"
|
||||
android:layout_alignParentBottom="true">
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/butFilter"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="@string/filter"
|
||||
android:scaleType="fitXY"
|
||||
android:padding="3dp"
|
||||
app:srcCompat="@drawable/ic_filter_white"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:layout_width="15dp"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/butShowSettings"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:contentDescription="@string/show_feed_settings_label"
|
||||
android:scaleType="fitXY"
|
||||
android:padding="3dp"
|
||||
app:srcCompat="@drawable/ic_settings_white"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/counts"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="40dp"
|
||||
android:ellipsize="end"
|
||||
android:layout_weight="0.7"
|
||||
android:gravity="end|center_vertical"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/text_size_navdrawer"
|
||||
tools:text="123" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_rounded_corner_left" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_rounded_corner_right" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/coverHolder"
|
||||
android:layout_width="124dp"
|
||||
android:layout_height="124dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
app:cardBackgroundColor="@color/non_square_icon_background"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardPreventCornerOverlap="false"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgvCover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/coverHolder"
|
||||
android:layout_alignTop="@id/coverHolder"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="2"
|
||||
android:textColor="@color/white"
|
||||
style="@style/Podcini.TextView.Heading"
|
||||
tools:text="Podcast title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvAuthor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:shadowColor="@color/black"
|
||||
android:shadowRadius="2"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
tools:text="Podcast author" />
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/txtvFailure"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp"
|
||||
android:background="?attr/icon_red"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone"
|
||||
android:text="@string/refresh_failed_msg"
|
||||
tools:visibility="visible"
|
||||
tools:text="(!) Last refresh failed" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/left_action_icon"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_questionmark" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/baseline_arrow_left_alt_24" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/txtvInformation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorAccent"
|
||||
tools:visibility="visible"
|
||||
tools:text="(i) Information" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/baseline_arrow_right_alt_24" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/right_action_icon"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_questionmark" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsTextView
|
||||
android:id="@+id/txtvUpdatesDisabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="2dp"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:visibility="gone"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorAccent"
|
||||
tools:visibility="visible"
|
||||
tools:text="Updates disabled" />
|
||||
|
||||
</LinearLayout>
|
@ -136,13 +136,13 @@
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminateOnly="true"
|
||||
android:visibility="visible" />
|
||||
<!-- <ProgressBar-->
|
||||
<!-- android:id="@+id/progressBar"-->
|
||||
<!-- android:layout_width="wrap_content"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_centerInParent="true"-->
|
||||
<!-- android:indeterminateOnly="true"-->
|
||||
<!-- android:visibility="visible" />-->
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/subscriptions_add"
|
||||
|
@ -1,133 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/item_description_fragment"
|
||||
android:fillViewport="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/playtime_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvPodcastTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:layout_marginBottom="5dp"
|
||||
tools:text="Podcast" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/episodeDate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:maxLines="1"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
tools:text="Episode" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvEpisodeTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
android:textSize="18sp"
|
||||
tools:text="Episode" />
|
||||
|
||||
<ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/chapterButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_margin="20dp"
|
||||
android:background="@drawable/grey_border"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:minWidth="150dp"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/butPrevChapter"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/prev_chapter"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_chapter_prev" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chapters_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/chapters_label"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="@dimen/text_size_navdrawer" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/butNextChapter"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/next_chapter"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_chapter_next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/coverHolder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgvCover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="32dp"
|
||||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:src="@android:drawable/sym_def_app_icon" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -1,232 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/player_ui_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/external_player_height"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:gravity="start"
|
||||
android:text="Title"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="13sp"/>
|
||||
|
||||
<ac.mdiq.podcini.ui.view.ChapterSeekBar
|
||||
android:id="@+id/sbPosition"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:clickable="true"
|
||||
android:max="500"
|
||||
android:visibility="invisible"
|
||||
tools:progress="100" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.NoRelayoutTextView
|
||||
android:id="@+id/txtvPosition"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:text="@string/position_default_label"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="@dimen/text_size_micro"/>
|
||||
|
||||
<ac.mdiq.podcini.ui.view.NoRelayoutTextView
|
||||
android:id="@+id/txtvLength"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:textAlignment="textEnd"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="@string/position_default_label"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="@dimen/text_size_micro"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgvCover"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="match_parent"
|
||||
android:cropToPadding="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@color/non_square_icon_background"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/player_control"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.PlayButton
|
||||
android:id="@+id/butPlay"
|
||||
android:layout_width="@dimen/audioplayer_playercontrols_length_big"
|
||||
android:layout_height="@dimen/audioplayer_playercontrols_length_big"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="false"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pause_label"
|
||||
android:padding="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_play_48dp"
|
||||
tools:srcCompat="@drawable/ic_play_48dp"
|
||||
app:tint="@color/medium_gray"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/butRev"
|
||||
android:layout_width="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_height="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_centerVertical="false"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_toStartOf="@id/butPlay"
|
||||
android:layout_toLeftOf="@id/butPlay"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/rewind_label"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_fast_rewind"
|
||||
tools:srcCompat="@drawable/ic_fast_rewind"
|
||||
app:tint="@color/medium_gray"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvRev"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/butRev"
|
||||
android:layout_alignStart="@id/butRev"
|
||||
android:layout_alignLeft="@id/butRev"
|
||||
android:layout_alignEnd="@id/butRev"
|
||||
android:layout_alignRight="@id/butRev"
|
||||
android:clickable="false"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="30"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
|
||||
android:id="@+id/butPlaybackSpeed"
|
||||
android:layout_width="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_height="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_centerVertical="false"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_toStartOf="@id/butRev"
|
||||
android:layout_toLeftOf="@id/butRev"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/playback_speed"
|
||||
app:foregroundColor="@color/medium_gray"
|
||||
tools:srcCompat="@drawable/ic_playback_speed"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvPlaybackSpeed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/butPlaybackSpeed"
|
||||
android:layout_alignStart="@id/butPlaybackSpeed"
|
||||
android:layout_alignLeft="@id/butPlaybackSpeed"
|
||||
android:layout_alignEnd="@id/butPlaybackSpeed"
|
||||
android:layout_alignRight="@id/butPlaybackSpeed"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:text="1.00"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/butFF"
|
||||
android:layout_width="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_height="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_centerVertical="false"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginEnd="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_marginRight="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_toEndOf="@id/butPlay"
|
||||
android:layout_toRightOf="@id/butPlay"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/fast_forward_label"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_fast_forward"
|
||||
tools:srcCompat="@drawable/ic_fast_forward"
|
||||
app:tint="@color/medium_gray"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvFF"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/butFF"
|
||||
android:layout_alignStart="@id/butFF"
|
||||
android:layout_alignLeft="@id/butFF"
|
||||
android:layout_alignEnd="@id/butFF"
|
||||
android:layout_alignRight="@id/butFF"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:text="30"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/butSkip"
|
||||
android:layout_width="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_height="@dimen/audioplayer_playercontrols_length"
|
||||
android:layout_centerVertical="false"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_marginLeft="@dimen/audioplayer_playercontrols_margin"
|
||||
android:layout_toEndOf="@id/butFF"
|
||||
android:layout_toRightOf="@id/butFF"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/skip_episode_label"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_skip_48dp"
|
||||
tools:srcCompat="@drawable/ic_skip_48dp"
|
||||
app:tint="@color/medium_gray"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvSkip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/butSkip"
|
||||
android:layout_alignStart="@id/butSkip"
|
||||
android:layout_alignLeft="@id/butSkip"
|
||||
android:layout_alignEnd="@id/butSkip"
|
||||
android:layout_alignRight="@id/butSkip"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
@ -1,18 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/queue_fragment">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:elevation="0dp">
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
@ -21,90 +21,16 @@
|
||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator"/>
|
||||
|
||||
<LinearLayout
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/infobar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/toolbar"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:orientation="horizontal">
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/left_action_icon"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_questionmark" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/baseline_arrow_left_alt_24" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/info_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:layout_gravity="center_vertical"
|
||||
tools:text="12 Episodes - 12 hours" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/baseline_arrow_right_alt_24" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/right_action_icon"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/ic_questionmark" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/lazyColumn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="@dimen/additional_horizontal_spacing"
|
||||
android:layout_below="@id/appbar"/>
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<!-- <androidx.swiperefreshlayout.widget.SwipeRefreshLayout-->
|
||||
<!-- android:id="@+id/swipeRefresh"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- android:layout_below="@id/appbar">-->
|
||||
|
||||
<!-- <ac.mdiq.podcini.ui.view.EpisodesRecyclerView-->
|
||||
<!-- android:id="@+id/recyclerView"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- android:paddingHorizontal="@dimen/additional_horizontal_spacing" />-->
|
||||
|
||||
<!-- </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>-->
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminateOnly="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<include
|
||||
layout="@layout/multi_select_speed_dial" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/search_fragment">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
@ -29,40 +30,22 @@
|
||||
android:id="@+id/feed_title_chip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/appbar"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginRight="0dp"
|
||||
android:visibility="gone"
|
||||
app:closeIconVisible="true" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
style="?android:attr/progressBarStyle" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerViewFeeds"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/feed_title_chip"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/lazyColumn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/recyclerViewFeeds"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingHorizontal="@dimen/additional_horizontal_spacing" />
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<include
|
||||
layout="@layout/multi_select_speed_dial" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
17
changelog.md
17
changelog.md
@ -1,7 +1,24 @@
|
||||
# 6.8.2 (Preview release)
|
||||
|
||||
* AudioPlayerFragment got overhauled. migrated to Jetpack Compose and PlayUI and PlayerDetailed fragments are Removed
|
||||
* EpisodeInfo is now in Compose
|
||||
* SearchFragment shows episodes list in Compose
|
||||
* Episodes viewholder and adapter etc are removed
|
||||
* SwipeActions class stripped out of View related operations
|
||||
* more enhancements in Compose functionalities
|
||||
* still have known issues
|
||||
|
||||
# 6.8.1 (Preview release)
|
||||
|
||||
* made Queues view in Jetpack Compose
|
||||
* enhanced various Compose functionalities
|
||||
* not yet ready for serious usage
|
||||
|
||||
# 6.8.0 (Preview release)
|
||||
|
||||
* the Compose class of DownloadsC replaces the old Downloads view
|
||||
* FeedEpisodes, AllEpisodes, History, and OnlineFeed mostly migrated to Jetpack Compose
|
||||
* there are still known issues and missing functions
|
||||
|
||||
# 6.7.3
|
||||
|
||||
|
5
fastlane/metadata/android/en-US/changelogs/3020258.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/3020258.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Version 6.8.1 (preview release):
|
||||
|
||||
* made Queues view in Jetpack Compose
|
||||
* enhanced various Compose functionalities
|
||||
* not yet ready for serious usage
|
9
fastlane/metadata/android/en-US/changelogs/3020259.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/3020259.txt
Normal file
@ -0,0 +1,9 @@
|
||||
Version 6.8.2 (preview release):
|
||||
|
||||
* AudioPlayerFragment got overhauled. migrated to Jetpack Compose and PlayUI and PlayerDetailed fragments are Removed
|
||||
* EpisodeInfo is now in Compose
|
||||
* SearchFragment shows episodes list in Compose
|
||||
* Episodes viewholder and adapter etc are removed
|
||||
* SwipeActions class stripped out of View related operations
|
||||
* more enhancements in Compose functionalities
|
||||
* still have known issues
|
Loading…
x
Reference in New Issue
Block a user