added about cast in readme

This commit is contained in:
Xilin Jia 2024-09-25 20:23:21 +01:00
parent 8b4747bc42
commit 0b68607bd9
4 changed files with 94 additions and 30 deletions

View File

@ -15,6 +15,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
#### Podcini.R version 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Since 6.6, podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs #### Podcini.R version 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Since 6.6, podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs
That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs) That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs)
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions. #### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
#### If you need to cast to an external speaker, you should install the "play" apk, not the "free" apk, that's about the difference between the two.
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions. #### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024. This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.

View File

@ -58,7 +58,8 @@ import kotlin.math.sin
open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private val tag: String) open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private val tag: String)
: ItemTouchHelper.SimpleCallback(dragDirs, ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT), DefaultLifecycleObserver { : ItemTouchHelper.SimpleCallback(dragDirs, ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT), DefaultLifecycleObserver {
private var filter: EpisodeFilter? = null @set:JvmName("setFilterProperty")
var filter: EpisodeFilter? = null
var actions: Actions? = null var actions: Actions? = null
var swipeOutEnabled: Boolean = true var swipeOutEnabled: Boolean = true
@ -79,6 +80,7 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
actions = null actions = null
} }
@JvmName("setFilterFunction")
fun setFilter(filter: EpisodeFilter?) { fun setFilter(filter: EpisodeFilter?) {
this.filter = filter this.filter = filter
} }
@ -186,7 +188,8 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
return if (swipeOutEnabled) 0.6f else 1.0f return if (swipeOutEnabled) 0.6f else 1.0f
} }
@UnstableApi override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { @UnstableApi
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder) super.clearView(recyclerView, viewHolder)
if (swipedOutTo != 0) { if (swipedOutTo != 0) {

View File

@ -8,15 +8,19 @@ import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.Queues import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.ImageResourceUtils import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler.PutToQueueDialog 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.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
import ac.mdiq.podcini.ui.utils.LocalDeleteModal import ac.mdiq.podcini.ui.utils.LocalDeleteModal
import ac.mdiq.podcini.ui.view.EpisodeViewHolder
import ac.mdiq.podcini.ui.view.EpisodeViewHolder.Companion
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import android.text.format.Formatter import android.text.format.Formatter
@ -49,24 +53,25 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import coil.compose.AsyncImage import coil.compose.AsyncImage
import kotlinx.coroutines.launch import io.realm.kotlin.notifications.SingleQueryChange
import io.realm.kotlin.notifications.UpdatedObject
import kotlinx.coroutines.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
fun InforBar(text: MutableState<String>, leftActionConfig: () -> Unit, rightActionConfig: () -> Unit) { fun InforBar(text: MutableState<String>, leftAction: MutableState<SwipeAction?>, rightAction: MutableState<SwipeAction?>, actionConfig: () -> Unit) {
// val textState by remember { mutableStateOf(text) }
val textColor = MaterialTheme.colors.onSurface val textColor = MaterialTheme.colors.onSurface
Logd("InforBar", "textState: ${text.value}") Logd("InforBar", "textState: ${text.value}")
Row { Row {
Image(painter = painterResource(R.drawable.ic_questionmark), contentDescription = "left_action_icon", Image(painter = painterResource(leftAction.value?.getActionIcon() ?:R.drawable.ic_questionmark), contentDescription = "left_action_icon",
Modifier.width(24.dp).height(24.dp).clickable(onClick = leftActionConfig)) 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)) Image(painter = painterResource(R.drawable.baseline_arrow_left_alt_24), contentDescription = "left_arrow", Modifier.width(24.dp).height(24.dp))
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text(text.value, color = textColor, style = MaterialTheme.typography.body2) Text(text.value, color = textColor, style = MaterialTheme.typography.body2)
Spacer(modifier = Modifier.weight(1f)) 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(R.drawable.baseline_arrow_right_alt_24), contentDescription = "right_arrow", Modifier.width(24.dp).height(24.dp))
Image(painter = painterResource(R.drawable.ic_questionmark), contentDescription = "right_action_icon", Image(painter = painterResource(rightAction.value?.getActionIcon() ?:R.drawable.ic_questionmark), contentDescription = "right_action_icon",
Modifier.width(24.dp).height(24.dp).clickable(onClick = rightActionConfig)) Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig))
} }
} }
@ -144,10 +149,8 @@ fun EpisodeSpeedDialOptions(activity: MainActivity, selected: List<Episode>): Li
} }
@Composable @Composable
fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episode>, leftAction: (Episode) -> Unit, rightAction: (Episode) -> Unit) { fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episode>, leftActionCB: (Episode) -> Unit, rightActionCB: (Episode) -> Unit) {
var selectMode by remember { mutableStateOf(false) } var selectMode by remember { mutableStateOf(false) }
var longPressedItem by remember { mutableStateOf<Episode?>(null) }
var longPressedPosition by remember { mutableStateOf(0) }
val selectedIds by remember { mutableStateOf(mutableSetOf<Long>()) } val selectedIds by remember { mutableStateOf(mutableSetOf<Long>()) }
val selected = remember { mutableListOf<Episode>()} val selected = remember { mutableListOf<Episode>()}
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -173,10 +176,10 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
if (velocity > 1000f || velocity < -1000f) { if (velocity > 1000f || velocity < -1000f) {
if (velocity > 0) { if (velocity > 0) {
Logd("EpisodeLazyColumn","Fling to the right with velocity: $velocity") Logd("EpisodeLazyColumn","Fling to the right with velocity: $velocity")
rightAction(episode) rightActionCB(episode)
} else { } else {
Logd("EpisodeLazyColumn","Fling to the left with velocity: $velocity") Logd("EpisodeLazyColumn","Fling to the left with velocity: $velocity")
leftAction(episode) leftActionCB(episode)
} }
} }
offsetX.animateTo( offsetX.animateTo(
@ -216,8 +219,6 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
selectedIds.clear() selectedIds.clear()
} }
Logd("EpisodeLazyColumn", "long clicked: ${episode.title}") Logd("EpisodeLazyColumn", "long clicked: ${episode.title}")
longPressedItem = episode
longPressedPosition = index
}, },
iconOnClick = { iconOnClick = {
Logd("EpisodeLazyColumn", "icon clicked!") Logd("EpisodeLazyColumn", "icon clicked!")
@ -249,8 +250,65 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun EpisodeRow(episode: Episode, isSelected: MutableState<Boolean>, onClick: () -> Unit, onLongClick: () -> Unit, iconOnClick: () -> Unit = {}) { fun EpisodeRow(episode_: Episode, isSelected: MutableState<Boolean>, onClick: () -> Unit, onLongClick: () -> Unit, iconOnClick: () -> Unit = {}) {
var episode = episode_
val textColor = MaterialTheme.colors.onSurface val textColor = MaterialTheme.colors.onSurface
var positionState by remember(episode) { mutableStateOf(episode.media?.position?:0) }
var playedState by remember { mutableStateOf(episode.isPlayed()) }
var farvoriteState by remember { mutableStateOf(episode.isFavorite) }
var inProgressState by remember { mutableStateOf(episode.isInProgress) }
var episodeMonitor: Job? by remember { mutableStateOf(null) }
var mediaMonitor: Job? by remember { mutableStateOf(null) }
if (episodeMonitor == null) {
val item_ = realm.query(Episode::class).query("id == ${episode.id}").first()
episodeMonitor = CoroutineScope(Dispatchers.Default).launch {
val episodeFlow = item_.asFlow()
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
when (changes) {
is UpdatedObject -> {
Logd("EpisodeRow", "episodeMonitor UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}")
episode = changes.obj
playedState = episode.isPlayed()
farvoriteState = episode.isFavorite
// withContext(Dispatchers.Main) {
//// bind(changes.obj)
//// if (posIndex >= 0) refreshAdapterPosCallback?.invoke(posIndex, changes.obj)
// }
}
else -> {}
}
}
}
}
if (mediaMonitor == null) {
val item_ = realm.query(Episode::class).query("id == ${episode.id}").first()
mediaMonitor = CoroutineScope(Dispatchers.Default).launch {
val episodeFlow = item_.asFlow(listOf("media.*"))
episodeFlow.collect { changes: SingleQueryChange<Episode> ->
when (changes) {
is UpdatedObject -> {
Logd("EpisodeRow", "mediaMonitor UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}")
episode = changes.obj
positionState = episode.media?.position?:0
inProgressState = episode.isInProgress
// withContext(Dispatchers.Main) {
//// updatePlaybackPositionNew(changes.obj)
////// bind(changes.obj)
//// if (posIndex >= 0) refreshAdapterPosCallback?.invoke(posIndex, changes.obj)
// }
}
else -> {}
}
}
}
}
DisposableEffect(Unit) {
onDispose {
episodeMonitor?.cancel()
mediaMonitor?.cancel()
}
}
Row (Modifier.background(if (isSelected.value) MaterialTheme.colors.secondary else MaterialTheme.colors.surface)) { Row (Modifier.background(if (isSelected.value) MaterialTheme.colors.secondary else MaterialTheme.colors.surface)) {
if (false) { if (false) {
val typedValue = TypedValue() val typedValue = TypedValue()
@ -260,8 +318,6 @@ fun EpisodeRow(episode: Episode, isSelected: MutableState<Boolean>, onClick: ()
modifier = Modifier.width(16.dp).align(Alignment.CenterVertically)) modifier = Modifier.width(16.dp).align(Alignment.CenterVertically))
} }
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
var playedState by remember { mutableStateOf(false) }
playedState = episode.isPlayed()
Logd("EpisodeRow", "playedState: $playedState") Logd("EpisodeRow", "playedState: $playedState")
val (image1, image2) = createRefs() val (image1, image2) = createRefs()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode) val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode)
@ -286,7 +342,7 @@ fun EpisodeRow(episode: Episode, isSelected: MutableState<Boolean>, onClick: ()
Row { Row {
if (episode.media?.getMediaType() == MediaType.VIDEO) if (episode.media?.getMediaType() == MediaType.VIDEO)
Image(painter = painterResource(R.drawable.ic_videocam), contentDescription = "isVideo", Modifier.width(14.dp).height(14.dp)) Image(painter = painterResource(R.drawable.ic_videocam), contentDescription = "isVideo", Modifier.width(14.dp).height(14.dp))
if (episode.isFavorite) if (farvoriteState)
Image(painter = painterResource(R.drawable.ic_star), contentDescription = "isFavorite", Modifier.width(14.dp).height(14.dp)) Image(painter = painterResource(R.drawable.ic_star), contentDescription = "isFavorite", Modifier.width(14.dp).height(14.dp))
if (curQueue.contains(episode)) if (curQueue.contains(episode))
Image(painter = painterResource(R.drawable.ic_playlist_play), contentDescription = "ivInPlaylist", Modifier.width(14.dp).height(14.dp)) Image(painter = painterResource(R.drawable.ic_playlist_play), contentDescription = "ivInPlaylist", Modifier.width(14.dp).height(14.dp))
@ -296,9 +352,9 @@ fun EpisodeRow(episode: Episode, isSelected: MutableState<Boolean>, onClick: ()
Text(if((episode.media?.size?:0) > 0) Formatter.formatShortFileSize(LocalContext.current, episode.media!!.size) else "", color = textColor, style = MaterialTheme.typography.body2) Text(if((episode.media?.size?:0) > 0) Formatter.formatShortFileSize(LocalContext.current, episode.media!!.size) else "", color = textColor, style = MaterialTheme.typography.body2)
} }
Text(episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis) Text(episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
if (InTheatre.isCurMedia(episode.media) || episode.isInProgress) { if (InTheatre.isCurMedia(episode.media) || inProgressState) {
val pos = episode.media!!.getPosition() val pos = positionState
val dur = episode.media!!.getDuration() val dur = remember(episode, episode.media) { episode.media!!.getDuration()}
val prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f val prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
Row { Row {
Text(DurationConverter.getDurationStringLong(pos), color = textColor) Text(DurationConverter.getDurationStringLong(pos), color = textColor)

View File

@ -16,6 +16,7 @@ import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.CustomTheme
@ -36,8 +37,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
@ -65,6 +65,8 @@ import java.util.*
private var episodes = mutableStateListOf<Episode>() private var episodes = mutableStateListOf<Episode>()
private var infoBarText = mutableStateOf("") private var infoBarText = mutableStateOf("")
var leftActionState = mutableStateOf<SwipeAction?>(null)
var rightActionState = mutableStateOf<SwipeAction?>(null)
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
// private lateinit var recyclerView: EpisodesRecyclerView // private lateinit var recyclerView: EpisodesRecyclerView
@ -94,17 +96,18 @@ import java.util.*
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
swipeActions = SwipeActions(this, TAG) swipeActions = SwipeActions(this, TAG)
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name))
binding.infobar.setContent { binding.infobar.setContent {
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
InforBar(infoBarText, leftActionConfig = {swipeActions.showDialog()}, rightActionConfig = { swipeActions.showDialog() }) InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
} }
} }
binding.lazyColumn.setContent { binding.lazyColumn.setContent {
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
EpisodeLazyColumn(activity as MainActivity, episodes = episodes, EpisodeLazyColumn(activity as MainActivity, episodes = episodes,
leftAction = { swipeActions.actions?.left?.performAction(it, this, EpisodeFilter())}, leftActionCB = { leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())},
rightAction = { swipeActions.actions?.right?.performAction(it, this, EpisodeFilter())}) rightActionCB = { rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())})
} }
} }
// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) // recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
@ -112,8 +115,7 @@ import java.util.*
// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) // recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
// swipeActions = SwipeActions(this, TAG).attachTo(recyclerView) // swipeActions = SwipeActions(this, TAG).attachTo(recyclerView)
lifecycle.addObserver(swipeActions) // lifecycle.addObserver(swipeActions)
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name))
refreshSwipeTelltale() refreshSwipeTelltale()
// binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() } // binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() }
// binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() } // binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() }
@ -363,6 +365,8 @@ import java.util.*
} }
private fun refreshSwipeTelltale() { private fun refreshSwipeTelltale() {
leftActionState.value = swipeActions.actions?.left
rightActionState.value = swipeActions.actions?.right
// if (swipeActions.actions?.left != null) binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon()) // if (swipeActions.actions?.left != null) binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
// if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon()) // if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
} }