diff --git a/README.md b/README.md index d39e018d..e051a0e5 100644 --- a/README.md +++ b/README.md @@ -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 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. +#### 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. This project was developed from a fork of [AntennaPod]() as of Feb 5 2024. diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt index 31b2d000..08825fd1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt @@ -58,7 +58,8 @@ 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 { - private var filter: EpisodeFilter? = null + @set:JvmName("setFilterProperty") + var filter: EpisodeFilter? = null var actions: Actions? = null var swipeOutEnabled: Boolean = true @@ -79,6 +80,7 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v actions = null } + @JvmName("setFilterFunction") fun setFilter(filter: EpisodeFilter?) { 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 } - @UnstableApi override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + @UnstableApi + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder) if (swipedOutTo != 0) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Episodes.kt index 3d66d6c2..654d8b76 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Episodes.kt @@ -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.Queues 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.MediaType import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.ImageResourceUtils 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.FeedInfoFragment 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.MiscFormatter.formatAbbrev import android.text.format.Formatter @@ -49,24 +53,25 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout 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 @Composable -fun InforBar(text: MutableState, leftActionConfig: () -> Unit, rightActionConfig: () -> Unit) { -// val textState by remember { mutableStateOf(text) } +fun InforBar(text: MutableState, leftAction: MutableState, rightAction: MutableState, actionConfig: () -> Unit) { val textColor = MaterialTheme.colors.onSurface Logd("InforBar", "textState: ${text.value}") Row { - Image(painter = painterResource(R.drawable.ic_questionmark), contentDescription = "left_action_icon", - Modifier.width(24.dp).height(24.dp).clickable(onClick = leftActionConfig)) + 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)) 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(R.drawable.ic_questionmark), contentDescription = "right_action_icon", - Modifier.width(24.dp).height(24.dp).clickable(onClick = rightActionConfig)) + Image(painter = painterResource(rightAction.value?.getActionIcon() ?:R.drawable.ic_questionmark), contentDescription = "right_action_icon", + Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig)) } } @@ -144,10 +149,8 @@ fun EpisodeSpeedDialOptions(activity: MainActivity, selected: List): Li } @Composable -fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList, leftAction: (Episode) -> Unit, rightAction: (Episode) -> Unit) { +fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList, leftActionCB: (Episode) -> Unit, rightActionCB: (Episode) -> Unit) { var selectMode by remember { mutableStateOf(false) } - var longPressedItem by remember { mutableStateOf(null) } - var longPressedPosition by remember { mutableStateOf(0) } val selectedIds by remember { mutableStateOf(mutableSetOf()) } val selected = remember { mutableListOf()} val coroutineScope = rememberCoroutineScope() @@ -173,10 +176,10 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList 1000f || velocity < -1000f) { if (velocity > 0) { Logd("EpisodeLazyColumn","Fling to the right with velocity: $velocity") - rightAction(episode) + rightActionCB(episode) } else { Logd("EpisodeLazyColumn","Fling to the left with velocity: $velocity") - leftAction(episode) + leftActionCB(episode) } } offsetX.animateTo( @@ -216,8 +219,6 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList, onClick: () -> Unit, onLongClick: () -> Unit, iconOnClick: () -> Unit = {}) { +fun EpisodeRow(episode_: Episode, isSelected: MutableState, onClick: () -> Unit, onLongClick: () -> Unit, iconOnClick: () -> Unit = {}) { + var episode = episode_ 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 -> + 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 -> + 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)) { if (false) { val typedValue = TypedValue() @@ -260,8 +318,6 @@ fun EpisodeRow(episode: Episode, isSelected: MutableState, onClick: () modifier = Modifier.width(16.dp).align(Alignment.CenterVertically)) } ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { - var playedState by remember { mutableStateOf(false) } - playedState = episode.isPlayed() Logd("EpisodeRow", "playedState: $playedState") val (image1, image2) = createRefs() val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode) @@ -286,7 +342,7 @@ fun EpisodeRow(episode: Episode, isSelected: MutableState, onClick: () Row { if (episode.media?.getMediaType() == MediaType.VIDEO) 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)) if (curQueue.contains(episode)) 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, onClick: () 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) - if (InTheatre.isCurMedia(episode.media) || episode.isInProgress) { - val pos = episode.media!!.getPosition() - val dur = episode.media!!.getDuration() + if (InTheatre.isCurMedia(episode.media) || inProgressState) { + val pos = positionState + val dur = remember(episode, episode.media) { episode.media!!.getDuration()} val prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f Row { Text(DurationConverter.getDurationStringLong(pos), color = textColor) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsCFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsCFragment.kt index 51e42d9a..38f4e806 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsCFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsCFragment.kt @@ -16,6 +16,7 @@ import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.utils.EpisodeUtil +import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.compose.CustomTheme @@ -36,8 +37,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.widget.Toolbar -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi @@ -65,6 +65,8 @@ import java.util.* private var episodes = mutableStateListOf() private var infoBarText = mutableStateOf("") + var leftActionState = mutableStateOf(null) + var rightActionState = mutableStateOf(null) private lateinit var toolbar: MaterialToolbar // private lateinit var recyclerView: EpisodesRecyclerView @@ -94,17 +96,18 @@ import java.util.* (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) swipeActions = SwipeActions(this, TAG) + swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name)) binding.infobar.setContent { CustomTheme(requireContext()) { - InforBar(infoBarText, leftActionConfig = {swipeActions.showDialog()}, rightActionConfig = { swipeActions.showDialog() }) + InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) } } binding.lazyColumn.setContent { CustomTheme(requireContext()) { EpisodeLazyColumn(activity as MainActivity, episodes = episodes, - leftAction = { swipeActions.actions?.left?.performAction(it, this, EpisodeFilter())}, - rightAction = { swipeActions.actions?.right?.performAction(it, this, EpisodeFilter())}) + leftActionCB = { leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())}, + rightActionCB = { rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())}) } } // recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) @@ -112,8 +115,7 @@ import java.util.* // recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) // swipeActions = SwipeActions(this, TAG).attachTo(recyclerView) - lifecycle.addObserver(swipeActions) - swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name)) +// lifecycle.addObserver(swipeActions) refreshSwipeTelltale() // binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() } // binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() } @@ -363,6 +365,8 @@ import java.util.* } 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?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon()) }