diff --git a/app/build.gradle b/app/build.gradle index f972a350..2c78693a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020256 - versionName "6.7.3" + versionCode 3020257 + versionName "6.8.0" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 6cbf6192..576634b4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -285,8 +285,9 @@ object Episodes { } if (resetMediaPosition) it.media?.setPosition(0) } + Logd(TAG, "setPlayStateSync played0: ${result.playState}") if (played == PlayState.PLAYED.code && shouldMarkedPlayedRemoveFromQueues()) removeFromAllQueuesSync(result) - Logd(TAG, "setPlayStateSync played: ${result.playState}") + Logd(TAG, "setPlayStateSync played1: ${result.playState}") EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result)) return result } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt index 395956cf..21455464 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt @@ -256,11 +256,13 @@ class Episode : RealmObject { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Episode) return false - return id == other.id + return id == other.id && playState == other.playState } override fun hashCode(): Int { - return (id xor (id ushr 32)).toInt() + var result = (id xor (id ushr 32)).toInt() + result = 31 * result + playState.hashCode() + return result } enum class PlayState(val code: Int) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt index c0b3bd56..c16cc671 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt @@ -49,11 +49,7 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis val composeView = ComposeView(context).apply { setContent { val showDialog = remember { mutableStateOf(true) } - CustomTheme(context) { - AltActionsDialog(context, showDialog.value, onDismiss = { - showDialog.value = false - }) - } + CustomTheme(context) { AltActionsDialog(context, showDialog.value, onDismiss = { showDialog.value = false }) } } } (button as? ViewGroup)?.addView(composeView) @@ -66,69 +62,38 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) { if (showDialog) { Dialog(onDismissRequest = onDismiss) { - Card(modifier = Modifier - .wrapContentSize(align = Alignment.Center) - .padding(16.dp), - shape = RoundedCornerShape(16.dp), - ) { - Row(modifier = Modifier.padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) { + Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { val label = getLabel() if (label != R.string.play_label && label != R.string.pause_label && label != R.string.download_label) { IconButton(onClick = { PlayActionButton(item).onClick(context) onDismiss() - }) { - Image( - painter = painterResource(R.drawable.ic_play_24dp), - contentDescription = "Play" - ) - } + }) { Image(painter = painterResource(R.drawable.ic_play_24dp), contentDescription = "Play") } } - if (label != R.string.stream_label && label != R.string.pause_label) { + if (label != R.string.stream_label && label != R.string.play_label && label != R.string.pause_label && label != R.string.delete_label) { IconButton(onClick = { StreamActionButton(item).onClick(context) onDismiss() - }) { - Image( - painter = painterResource(R.drawable.ic_stream), - contentDescription = "Stream" - ) - } + }) { Image(painter = painterResource(R.drawable.ic_stream), contentDescription = "Stream") } } - if (label != R.string.download_label && label != R.string.play_label) { + if (label != R.string.download_label && label != R.string.play_label && label != R.string.delete_label) { IconButton(onClick = { DownloadActionButton(item).onClick(context) onDismiss() - }) { - Image( - painter = painterResource(R.drawable.ic_download), - contentDescription = "Download" - ) - } + }) { Image(painter = painterResource(R.drawable.ic_download), contentDescription = "Download") } } if (label != R.string.delete_label && label != R.string.download_label && label != R.string.stream_label) { IconButton(onClick = { DeleteActionButton(item).onClick(context) onDismiss() - }) { - Image( - painter = painterResource(R.drawable.ic_delete), - contentDescription = "Delete" - ) - } + }) { Image(painter = painterResource(R.drawable.ic_delete), contentDescription = "Delete") } } if (label != R.string.visit_website_label) { IconButton(onClick = { VisitWebsiteActionButton(item).onClick(context) onDismiss() - }) { - Image( - painter = painterResource(R.drawable.ic_web), - contentDescription = "Web" - ) - } + }) { Image(painter = painterResource(R.drawable.ic_web), contentDescription = "Web") } } } } 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 08825fd1..efa9337a 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 @@ -22,10 +22,7 @@ import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction.Companion.NO_ACTION import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog -import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment -import ac.mdiq.podcini.ui.fragment.DownloadsFragment -import ac.mdiq.podcini.ui.fragment.HistoryFragment -import ac.mdiq.podcini.ui.fragment.QueuesFragment +import ac.mdiq.podcini.ui.fragment.* import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr import ac.mdiq.podcini.ui.view.EpisodeViewHolder diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt index c9729ff0..36b92b57 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -418,7 +418,6 @@ class MainActivity : CastEnabledActivity() { QueuesFragment.TAG -> fragment = QueuesFragment() AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment() DownloadsFragment.TAG -> fragment = DownloadsFragment() - DownloadsCFragment.TAG -> fragment = DownloadsCFragment() HistoryFragment.TAG -> fragment = HistoryFragment() OnlineSearchFragment.TAG -> fragment = OnlineSearchFragment() SubscriptionsFragment.TAG -> fragment = SubscriptionsFragment() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt index 961e403e..4b7ceba6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt @@ -1,14 +1,8 @@ package ac.mdiq.podcini.ui.compose -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterialApi::class) @Composable @@ -51,35 +45,3 @@ fun Spinner( } } } - -@Composable -fun SpeedDial( - modifier: Modifier = Modifier, - mainButtonIcon: @Composable () -> Unit, - fabButtons: List<@Composable () -> Unit>, - onMainButtonClick: () -> Unit, - onFabButtonClick: (Int) -> Unit -) { - var isExpanded by remember { mutableStateOf(false) } - Column( - modifier = modifier, - verticalArrangement = Arrangement.Bottom - ) { - if (isExpanded) { - fabButtons.forEachIndexed { index, button -> - FloatingActionButton( - onClick = { onFabButtonClick(index) }, - modifier = Modifier.padding(bottom = 4.dp) - ) { - button() - } - } - } - FloatingActionButton( - onClick = { onMainButtonClick(); isExpanded = !isExpanded }, -// modifier = Modifier.padding(bottom = 16.dp) - ) { - mainButtonIcon() - } - } -} \ No newline at end of file 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 654d8b76..5609ad42 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 @@ -13,14 +13,13 @@ 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.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.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 @@ -29,9 +28,11 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit @@ -55,7 +56,10 @@ import androidx.constraintlayout.compose.ConstraintLayout import coil.compose.AsyncImage import io.realm.kotlin.notifications.SingleQueryChange import io.realm.kotlin.notifications.UpdatedObject -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable @@ -76,90 +80,158 @@ fun InforBar(text: MutableState, leftAction: MutableState, } @Composable -fun EpisodeSpeedDialOptions(activity: MainActivity, selected: List): List<@Composable () -> Unit> { - return listOf<@Composable () -> Unit>( +fun EpisodeSpeedDial(activity: MainActivity, selected: List, modifier: Modifier = Modifier) { + val TAG = "EpisodeSpeedDial" + var isExpanded by remember { mutableStateOf(false) } + val options = listOf<@Composable () -> Unit>( { Row(modifier = Modifier.padding(horizontal = 16.dp) .clickable { - Logd("EpisodeSpeedDialActions", "ic_delete: ${selected.size}") + 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 { - Logd("EpisodeSpeedDialActions", "ic_download: ${selected.size}") + 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 { - Logd("EpisodeSpeedDialActions", "ic_mark_played: ${selected.size}") + 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 { - Logd("EpisodeSpeedDialActions", "ic_playlist_remove: ${selected.size}") + 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 { - Logd("EpisodeSpeedDialActions", "ic_playlist_play: ${selected.size}") + 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 { - Logd("EpisodeSpeedDialActions", "ic_playlist_play: ${selected.size}") + 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 { - Logd("EpisodeSpeedDialActions", "ic_star: ${selected.size}") + 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) { + 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") } + } } +@OptIn(ExperimentalFoundationApi::class) @Composable -fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList, leftActionCB: (Episode) -> Unit, rightActionCB: (Episode) -> Unit) { +fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList, 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()) } val selected = remember { mutableListOf()} val coroutineScope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() Box(modifier = Modifier.fillMaxWidth()) { - LazyColumn(modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + LazyColumn(state = lazyListState, + modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp)) { itemsIndexed(episodes) { index, episode -> - val offsetX = remember { Animatable(0f) } + var positionState by remember { 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) { + episodeMonitor = CoroutineScope(Dispatchers.Default).launch { + val item_ = realm.query(Episode::class).query("id == ${episode.id}").first() + val episodeFlow = item_.asFlow() + episodeFlow.collect { changes: SingleQueryChange -> + 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") + } + else -> {} + } + } + } + } + if (mediaMonitor == null) { + mediaMonitor = CoroutineScope(Dispatchers.Default).launch { + val item_ = realm.query(Episode::class).query("id == ${episode.id}").first() + val episodeFlow = item_.asFlow(listOf("media.*")) + episodeFlow.collect { changes: SingleQueryChange -> + when (changes) { + is UpdatedObject -> { + Logd(TAG, "mediaMonitor UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}") + positionState = changes.obj.media?.position?:0 + inProgressState = changes.obj.isInProgress + episodes[index] = changes.obj + } + else -> {} + } + } + } + } + DisposableEffect(Unit) { + onDispose { + episodeMonitor?.cancel() + mediaMonitor?.cancel() + } + } val velocityTracker = remember { VelocityTracker() } + val offsetX = remember { Animatable(0f) } Box( modifier = Modifier .fillMaxWidth() @@ -174,13 +246,8 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList 1000f || velocity < -1000f) { - if (velocity > 0) { - Logd("EpisodeLazyColumn","Fling to the right with velocity: $velocity") - rightActionCB(episode) - } else { - Logd("EpisodeLazyColumn","Fling to the left with velocity: $velocity") - leftActionCB(episode) - } + if (velocity > 0) rightSwipeCB(episodes[index]) + else leftSwipeCB(episodes[index]) } offsetX.animateTo( targetValue = 0f, // Back to the initial position @@ -194,186 +261,97 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList 0) Formatter.formatShortFileSize(LocalContext.current, episode.media!!.size) else "" + Text(dateSizeText, color = textColor, style = MaterialTheme.typography.body2) } - Logd("EpisodeLazyColumn", "long clicked: ${episode.title}") - }, - iconOnClick = { - Logd("EpisodeLazyColumn", "icon clicked!") - if (selectMode) { - isSelected = !isSelected - if (isSelected) { - selectedIds.add(episode.id) - selected.add(episode) - } else { - selectedIds.remove(episode.id) - selected.remove(episode) + Text(episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis) + 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, style = MaterialTheme.typography.caption) + LinearProgressIndicator(progress = prog, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically)) + Text(DurationConverter.getDurationStringLong(dur), color = textColor, style = MaterialTheme.typography.caption) } - } else activity.loadChildFragment(FeedInfoFragment.newInstance(episode.feed!!)) - }) - } - } - } - if (selectMode) SpeedDial( - modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp), - mainButtonIcon = { Icon(Icons.Filled.Edit, "Edit") }, - fabButtons = EpisodeSpeedDialOptions(activity, selected), - onMainButtonClick = { }, - onFabButtonClick = { index -> - Logd("EpisodeLazyColumn", "onFabButtonClick: $index }") - } - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -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) -// } + } + } + var actionButton by remember { mutableStateOf(if (actionButton_ == null) EpisodeActionButton.forItem(episodes[index]) else actionButton_(episodes[index])) } + var showAltActionsDialog by remember { mutableStateOf(false) } + 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)) + } + if (showAltActionsDialog) actionButton.AltActionsDialog(activity, showAltActionsDialog, onDismiss = { showAltActionsDialog = false }) } - else -> {} } } } + if (selectMode) EpisodeSpeedDial(activity, selected, modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp)) } - 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() - LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true) - Image(painter = painterResource(typedValue.resourceId), - contentDescription = "drag handle", - modifier = Modifier.width(16.dp).align(Alignment.CenterVertically)) - } - ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { - Logd("EpisodeRow", "playedState: $playedState") - val (image1, image2) = createRefs() - val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode) - AsyncImage(model = imgLoc, contentDescription = "imgvCover", - Modifier.width(56.dp) - .height(56.dp) - .clickable(onClick = iconOnClick) - .constrainAs(image1) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - }) - 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(image2) { - bottom.linkTo(parent.bottom) - end.linkTo(parent.end) - }) - } - Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp) - .combinedClickable(onClick = onClick, onLongClick = onLongClick)) { - Row { - if (episode.media?.getMediaType() == MediaType.VIDEO) - Image(painter = painterResource(R.drawable.ic_videocam), contentDescription = "isVideo", 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)) - Text("·", color = textColor) - Text(formatAbbrev(LocalContext.current, episode.getPubDate()), color = textColor, style = MaterialTheme.typography.body2) - Text("·", color = textColor) - 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) || 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) - LinearProgressIndicator( - progress = prog, - modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically) - ) - Text(DurationConverter.getDurationStringLong(dur), color = textColor) - } - } - } - IconButton( - onClick = { /* Do something */ }, - Modifier.align(Alignment.CenterVertically) - ) { - Image( - painter = painterResource(R.drawable.ic_delete), - contentDescription = "Delete" - ) - } - } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/FeedEpisodesHeader.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/FeedEpisodesHeader.kt new file mode 100644 index 00000000..c85882dd --- /dev/null +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/FeedEpisodesHeader.kt @@ -0,0 +1,76 @@ +package ac.mdiq.podcini.ui.compose + +import ac.mdiq.podcini.R +import ac.mdiq.podcini.storage.model.Feed +import ac.mdiq.podcini.ui.activity.MainActivity +import ac.mdiq.podcini.ui.fragment.FeedSettingsFragment +import ac.mdiq.podcini.ui.utils.TransitionEffect +import ac.mdiq.podcini.util.Logd +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import coil.compose.AsyncImage + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FeedEpisodesHeader(activity: MainActivity, feed: Feed?, filterButColor: Color, filterClickCB: ()->Unit, filterLongClickCB: ()->Unit) { + val TAG = "FeedEpisodesHeader" + val textColor = MaterialTheme.colors.onSurface + ConstraintLayout(modifier = Modifier.fillMaxWidth().height(120.dp)) { + val (controlRow, image1, image2, imgvCover, taColumn) = createRefs() + Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp).background(colorResource(id = R.color.image_readability_tint)) + .constrainAs(controlRow) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + }, verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.weight(1f)) + Image(painter = painterResource(R.drawable.ic_filter_white), colorFilter = ColorFilter.tint(filterButColor), contentDescription = "butFilter", + modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).combinedClickable(onClick = filterClickCB, onLongClick = filterLongClickCB)) + Spacer(modifier = Modifier.width(15.dp)) + Image(painter = painterResource(R.drawable.ic_settings_white), contentDescription = "butShowSettings", + Modifier.width(40.dp).height(40.dp).padding(3.dp).clickable(onClick = { + if (feed != null) { + val fragment = FeedSettingsFragment.newInstance(feed) + activity.loadChildFragment(fragment, TransitionEffect.SLIDE) + } + })) + Spacer(modifier = Modifier.weight(1f)) + Text(feed?.episodes?.size?.toString()?:"", textAlign = TextAlign.Center, color = Color.White, style = MaterialTheme.typography.body1) + } + Image(painter = painterResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner", + Modifier.width(12.dp).height(12.dp).constrainAs(image1) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + }) + Image(painter = painterResource(R.drawable.ic_rounded_corner_right), contentDescription = "right_corner", + Modifier.width(12.dp).height(12.dp).constrainAs(image2) { + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + }) + AsyncImage(model = feed?.imageUrl?:"", contentDescription = "imgvCover", + Modifier.width(120.dp).height(120.dp).padding(start = 16.dp, end = 16.dp, bottom = 12.dp).constrainAs(imgvCover) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + }.clickable(onClick = { + Logd(TAG, "icon clicked!") + })) + Column(Modifier.constrainAs(taColumn) { + top.linkTo(imgvCover.top) + start.linkTo(imgvCover.end) }) { + Text(feed?.title?:"", color = textColor, style = MaterialTheme.typography.body1, maxLines = 2, overflow = TextOverflow.Ellipsis) + Text(feed?.author?:"", color = textColor, style = MaterialTheme.typography.body2, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt index acc846c5..dfcd666c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt @@ -12,9 +12,9 @@ import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog -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.os.Bundle import android.view.LayoutInflater import android.view.MenuItem @@ -27,13 +27,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.apache.commons.lang3.StringUtils -import kotlin.math.min - -/** - * Shows all episodes (possibly filtered by user). - */ -@UnstableApi class AllEpisodesFragment : BaseEpisodesFragment() { +@UnstableApi +class AllEpisodesFragment : BaseEpisodesFragment() { private var allEpisodes: List = listOf() @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -43,10 +39,9 @@ import kotlin.math.min toolbar.inflateMenu(R.menu.episodes) toolbar.setTitle(R.string.episodes_label) updateToolbar() - txtvInformation.visibility = View.VISIBLE - txtvInformation.setOnClickListener { - AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null) - } +// txtvInformation.setOnClickListener { +// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null) +// } return root } @@ -76,14 +71,7 @@ import kotlin.math.min } if (allEpisodes.isEmpty()) return listOf() allEpisodes = allEpisodes.filter { filter.matchesForQueues(it) } - return allEpisodes.subList(0, min(allEpisodes.size, page * EPISODES_PER_PAGE)) - } - - override fun loadMoreData(page: Int): List { - val offset = (page - 1) * EPISODES_PER_PAGE - if (offset >= allEpisodes.size) return listOf() - val toIndex = offset + EPISODES_PER_PAGE - return allEpisodes.subList(offset, min(allEpisodes.size, toIndex)) + return allEpisodes } override fun loadTotalItemCount(): Int { @@ -146,13 +134,12 @@ import kotlin.math.min override fun updateToolbar() { swipeActions.setFilter(getFilter()) + var info = "${episodes.size} episodes" if (getFilter().values.isNotEmpty()) { - txtvInformation.text = "${adapter.totalNumberOfItems} episodes - ${getString(R.string.filtered_label)}" + info += " - ${getString(R.string.filtered_label)}" emptyView.setMessage(R.string.no_all_episodes_filtered_label) - } else { - txtvInformation.text = "${adapter.totalNumberOfItems} episodes" - emptyView.setMessage(R.string.no_all_episodes_label) - } + } else emptyView.setMessage(R.string.no_all_episodes_label) + infoBarText.value = info toolbar.menu?.findItem(R.id.action_favorites)?.setIcon(if (getFilter().showIsFavorite) R.drawable.ic_star else R.drawable.ic_star_border) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt index 9f894cbb..d44f88d6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt @@ -2,40 +2,30 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.BaseEpisodesListFragmentBinding -import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.utils.EpisodeUtil -import ac.mdiq.podcini.ui.actions.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.dialog.ConfirmationDialog +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.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 android.content.DialogInterface +import ac.mdiq.podcini.util.Logd import android.os.Bundle import android.util.Log import android.view.* -import android.widget.ProgressBar -import android.widget.TextView import androidx.appcompat.widget.Toolbar +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.core.util.Pair import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SimpleItemAnimator import com.google.android.material.appbar.MaterialToolbar -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 @@ -43,29 +33,26 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@UnstableApi abstract class BaseEpisodesFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { +@UnstableApi +abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener { val TAG = this::class.simpleName ?: "Anonymous" @JvmField protected var page: Int = 1 - protected var isLoadingMore: Boolean = false - protected var hasMoreItems: Boolean = false private var displayUpArrow = false var _binding: BaseEpisodesListFragmentBinding? = null protected val binding get() = _binding!! - lateinit var recyclerView: EpisodesRecyclerView + protected var infoBarText = mutableStateOf("") + private var leftActionState = mutableStateOf(null) + private var rightActionState = mutableStateOf(null) + lateinit var emptyView: EmptyViewHandler - lateinit var speedDialView: SpeedDialView lateinit var toolbar: MaterialToolbar lateinit var swipeActions: SwipeActions - private lateinit var progressBar: ProgressBar - lateinit var adapter: EpisodesAdapter - protected lateinit var txtvInformation: TextView - @JvmField - var episodes: MutableList = ArrayList() + val episodes = mutableStateListOf() @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) @@ -73,94 +60,91 @@ import kotlinx.coroutines.withContext _binding = BaseEpisodesListFragmentBinding.inflate(inflater) Logd(TAG, "fragment onCreateView") - txtvInformation = binding.txtvInformation 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) (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) - recyclerView = binding.recyclerView - recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) - setupLoadMoreScrollListener() - recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) +// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) +// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) - swipeActions = SwipeActions(this, TAG).attachTo(recyclerView) + swipeActions = SwipeActions(this, TAG) lifecycle.addObserver(swipeActions) + binding.infobar.setContent { + CustomTheme(requireContext()) { + InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) + } + } + binding.lazyColumn.setContent { + CustomTheme(requireContext()) { + EpisodeLazyColumn(activity as MainActivity, episodes = episodes, + 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())}, + ) + } + } + swipeActions.setFilter(getFilter()) refreshSwipeTelltale() - binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() } - binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() } - - val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator - if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false createListAdaptor() - progressBar = binding.progressBar - progressBar.visibility = View.VISIBLE - emptyView = EmptyViewHandler(requireContext()) - emptyView.attachToRecyclerView(recyclerView) emptyView.setIcon(R.drawable.ic_feed) emptyView.setTitle(R.string.no_all_episodes_head_label) emptyView.setMessage(R.string.no_all_episodes_label) - emptyView.updateAdapter(adapter) 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()) -// 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 +// 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()) +//// 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 } - open fun createListAdaptor() { - adapter = object : EpisodesAdapter(activity as MainActivity) {} - adapter.setOnSelectModeListener(this) - recyclerView.adapter = adapter - } + open fun createListAdaptor() {} override fun onStart() { super.onStart() @@ -171,25 +155,13 @@ import kotlinx.coroutines.withContext override fun onStop() { super.onStop() cancelFlowEvents() - val recyclerView = binding.recyclerView - 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 onResume() { - super.onResume() - registerForContextMenu(recyclerView) - } - - override fun onPause() { - super.onPause() - recyclerView.saveScrollPosition(getPrefName()) - unregisterForContextMenu(recyclerView) - } +// override fun onPause() { +// super.onPause() +//// recyclerView.saveScrollPosition(getPrefName()) +//// unregisterForContextMenu(recyclerView) +// } @Deprecated("Deprecated in Java") @UnstableApi override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -204,111 +176,28 @@ import kotlinx.coroutines.withContext } } - @UnstableApi private fun performMultiSelectAction(actionItemId: Int) { - val handler = EpisodeMultiSelectHandler((activity as MainActivity), actionItemId) - handler.handleAction(adapter.selectedItems) -// TODO: this appears incorrect, and lazy loading is likely not needed -// lifecycleScope.launch { -// try { -// withContext(Dispatchers.IO) { -// handler.handleAction(adapter.selectedItems.filterIsInstance()) -// if (adapter.shouldSelectLazyLoadedItems()) { -// var applyPage = page + 1 -// var nextPage: List -// do { -// nextPage = loadMoreData(applyPage) -// handler.handleAction(nextPage) -// applyPage++ -// } while (nextPage.size == EPISODES_PER_PAGE) -// } -// withContext(Dispatchers.Main) { -// adapter.endSelectMode() -// } -// } -// } catch (e: Throwable) { -// Log.e(TAG, Log.getStackTraceString(e)) -// } -// } - } - - private fun setupLoadMoreScrollListener() { - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(view: RecyclerView, deltaX: Int, deltaY: Int) { - super.onScrolled(view, deltaX, deltaY) -// Logd(TAG, "addOnScrollListener called isLoadingMore:$isLoadingMore hasMoreItems:$hasMoreItems ${recyclerView.isScrolledToBottom}") - if (!isLoadingMore && hasMoreItems && recyclerView.isScrolledToBottom) { - /* The end of the list has been reached. Load more data. */ - page++ - loadMoreItems() -// isLoadingMore = true - } - } - }) - } - - private fun loadMoreItems() { - Logd(TAG, "loadMoreItems() called $page") - - isLoadingMore = true -// listAdapter.setDummyViews(1) -// listAdapter.notifyItemInserted(listAdapter.itemCount - 1) - lifecycleScope.launch { - try { - val data = withContext(Dispatchers.IO) { - val data_ = loadMoreData(page) - if (data_.size < EPISODES_PER_PAGE) hasMoreItems = false - Logd(TAG, "loadMoreItems $page ${data_.size}") - episodes.addAll(data_) - data_ - } - withContext(Dispatchers.Main) { -// listAdapter.setDummyViews(0) - adapter.updateItems(episodes) - if (adapter.shouldSelectLazyLoadedItems()) adapter.setSelected(episodes.size - data.size, episodes.size, true) - } - } catch (e: Throwable) { -// listAdapter.setDummyViews(0) - adapter.updateItems(mutableListOf()) - Log.e(TAG, Log.getStackTraceString(e)) - } finally { - withContext(Dispatchers.Main) { recyclerView.post { isLoadingMore = false } } - } - } - } - override fun onDestroyView() { Logd(TAG, "onDestroyView") _binding = null - adapter.clearData() - adapter.endSelectMode() episodes.clear() super.onDestroyView() } - override fun onStartSelectMode() { - speedDialView.visibility = View.VISIBLE - } - - override fun onEndSelectMode() { - speedDialView.close() - speedDialView.visibility = View.GONE - } - 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) +// KeyEvent.KEYCODE_T -> recyclerView.smoothScrollToPosition(0) +// KeyEvent.KEYCODE_B -> recyclerView.smoothScrollToPosition(adapter.itemCount) else -> {} } } 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 (downloadUrl in event.urls) { +// val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl) +// if (pos >= 0) adapter.notifyItemChangedCompat(pos) +// } } private var eventSink: Job? = null @@ -352,8 +241,8 @@ import kotlinx.coroutines.withContext } 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 } private var loadItemsRunning = false @@ -361,7 +250,6 @@ import kotlinx.coroutines.withContext if (!loadItemsRunning) { loadItemsRunning = true Logd(TAG, "loadItems() called") - adapter.updateItems(mutableListOf()) lifecycleScope.launch { try { val data = withContext(Dispatchers.IO) { @@ -369,18 +257,12 @@ import kotlinx.coroutines.withContext } withContext(Dispatchers.Main) { val restoreScrollPosition = episodes.isEmpty() - episodes = data.first - hasMoreItems = !(page == 1 && episodes.size < EPISODES_PER_PAGE) - progressBar.visibility = View.GONE -// listAdapter.setDummyViews(0) - adapter.updateItems(episodes) - adapter.setTotalNumberOfItems(data.second) - if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName()) + episodes.clear() + episodes.addAll(data.first) +// if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName()) updateToolbar() } } catch (e: Throwable) { -// listAdapter.setDummyViews(0) - adapter.updateItems(mutableListOf()) Log.e(TAG, Log.getStackTraceString(e)) } finally { loadItemsRunning = false @@ -391,8 +273,6 @@ import kotlinx.coroutines.withContext protected abstract fun loadData(): List - protected abstract fun loadMoreData(page: Int): List - protected abstract fun loadTotalItemCount(): Int open fun getFilter(): EpisodeFilter { @@ -403,10 +283,6 @@ import kotlinx.coroutines.withContext protected open fun updateToolbar() {} -// private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdatingEvent) { -// swipeRefreshLayout.isRefreshing = event.isRunning -// } - override fun onSaveInstanceState(outState: Bundle) { outState.putBoolean(KEY_UP_ARROW, displayUpArrow) super.onSaveInstanceState(outState) 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 deleted file mode 100644 index 38f4e806..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsCFragment.kt +++ /dev/null @@ -1,512 +0,0 @@ -package ac.mdiq.podcini.ui.fragment - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.DownloadsFragmentBinding -import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding -import ac.mdiq.podcini.net.download.service.DownloadServiceInterface -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.appPrefs -import ac.mdiq.podcini.storage.database.Episodes.getEpisodes -import ac.mdiq.podcini.storage.database.RealmDB.realm -import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope -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.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 -import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn -import ac.mdiq.podcini.ui.compose.InforBar -import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog -import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog -import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog -import ac.mdiq.podcini.ui.utils.EmptyViewHandler -import ac.mdiq.podcini.util.EventFlow -import ac.mdiq.podcini.util.FlowEvent -import ac.mdiq.podcini.util.Logd -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.appcompat.widget.Toolbar -import androidx.compose.runtime.* -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import androidx.media3.common.util.UnstableApi -import com.google.android.material.appbar.MaterialToolbar -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 -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.apache.commons.lang3.StringUtils -import java.io.File -import java.util.* - -/** - * Displays all completed downloads and provides a button to delete them. - */ -@UnstableApi class DownloadsCFragment : Fragment(), Toolbar.OnMenuItemClickListener { - - private var _binding: DownloadsFragmentBinding? = null - private val binding get() = _binding!! - - private var runningDownloads: Set = HashSet() - 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 - private lateinit var swipeActions: SwipeActions - private lateinit var speedDialView: SpeedDialView - private lateinit var emptyView: EmptyViewHandler - - private var displayUpArrow = false - - @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = DownloadsFragmentBinding.inflate(inflater) - - Logd(TAG, "fragment onCreateView") - toolbar = binding.toolbar -// toolbar.setTitle(R.string.downloadsC_label) - toolbar.setTitle("Preview only") - toolbar.inflateMenu(R.menu.downloads_completed) - toolbar.setOnMenuItemClickListener(this) - toolbar.setOnLongClickListener { -// recyclerView.scrollToPosition(5) -// recyclerView.post { recyclerView.smoothScrollToPosition(0) } - false - } - displayUpArrow = parentFragmentManager.backStackEntryCount != 0 - if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) - - (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) - - swipeActions = SwipeActions(this, TAG) - swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name)) - binding.infobar.setContent { - CustomTheme(requireContext()) { - InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) - } - } - - binding.lazyColumn.setContent { - CustomTheme(requireContext()) { - EpisodeLazyColumn(activity as MainActivity, episodes = episodes, - leftActionCB = { leftActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())}, - rightActionCB = { rightActionState.value?.performAction(it, this, swipeActions.filter ?: EpisodeFilter())}) - } - } -// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) -// adapter.setOnSelectModeListener(this) -// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) - -// swipeActions = SwipeActions(this, TAG).attachTo(recyclerView) -// lifecycle.addObserver(swipeActions) - refreshSwipeTelltale() -// binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() } -// binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() } - -// val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator -// if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false - - val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root) - speedDialView = multiSelectDial.fabSD - speedDialView.overlayLayout = multiSelectDial.fabSDOverlay - speedDialView.inflate(R.menu.episodes_apply_action_speeddial) - speedDialView.removeActionItemById(R.id.download_batch) - speedDialView.removeActionItemById(R.id.remove_from_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 -> -// adapter.selectedItems.let { -// EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(it) -// } -// adapter.endSelectMode() - true - } - if (arguments != null && requireArguments().getBoolean(ARG_SHOW_LOGS, false)) DownloadLogFragment().show(childFragmentManager, null) - - addEmptyView() - return binding.root - } - - fun leftAction(episode: Episode) { - swipeActions.actions?.left?.performAction(episode, this, EpisodeFilter()) - } - - override fun onStart() { - super.onStart() - procFlowEvents() - loadItems() - } - - override fun onStop() { - super.onStop() - cancelFlowEvents() -// val recyclerView = binding.recyclerView -// 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 onSaveInstanceState(outState: Bundle) { - outState.putBoolean(KEY_UP_ARROW, displayUpArrow) - super.onSaveInstanceState(outState) - } - - override fun onDestroyView() { - Logd(TAG, "onDestroyView") - _binding = null -// adapter.endSelectMode() -// adapter.clearData() - toolbar.setOnMenuItemClickListener(null) - toolbar.setOnLongClickListener(null) - episodes.clear() - - super.onDestroyView() - } - - @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.filter_items -> DownloadsFilterDialog.newInstance(getFilter()).show(childFragmentManager, null) - R.id.action_download_logs -> DownloadLogFragment().show(childFragmentManager, null) - R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) - R.id.downloads_sort -> DownloadsSortDialog().show(childFragmentManager, "SortDialog") - R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show() - R.id.reconcile -> reconcile() - else -> return false - } - return true - } - - private fun getFilter(): EpisodeFilter { - return EpisodeFilter(prefFilterDownloads) - } - - private val nameEpisodeMap: MutableMap = mutableMapOf() - private val filesRemoved: MutableList = mutableListOf() - private fun reconcile() { - runOnIOScope { - val items = realm.query(Episode::class).query("media.episode == nil").find() - Logd(TAG, "number of episode with null backlink: ${items.size}") - for (item in items) { - upsert(item) { it.media!!.episode = it } - } - nameEpisodeMap.clear() - for (e in episodes) { - var fileUrl = e.media?.fileUrl ?: continue - fileUrl = fileUrl.substring(fileUrl.lastIndexOf('/') + 1) - Logd(TAG, "reconcile: fileUrl: $fileUrl") - nameEpisodeMap[fileUrl] = e - } - val mediaDir = requireContext().getExternalFilesDir("media") ?: return@runOnIOScope - mediaDir.listFiles()?.forEach { file -> traverse(file, mediaDir) } - Logd(TAG, "reconcile: end, episodes missing file: ${nameEpisodeMap.size}") - if (nameEpisodeMap.isNotEmpty()) { - for (e in nameEpisodeMap.values) { - upsertBlk(e) { it.media?.setfileUrlOrNull(null) } - } - } - loadItems() - Logd(TAG, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}") - withContext(Dispatchers.Main) { - Toast.makeText(requireContext(), "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}", Toast.LENGTH_LONG).show() - } - } - } - - private fun traverse(srcFile: File, srcRootDir: File) { - val relativePath = srcFile.absolutePath.substring(srcRootDir.absolutePath.length+1) - if (srcFile.isDirectory) { - Logd(TAG, "traverse folder title: $relativePath") - val dirFiles = srcFile.listFiles() - dirFiles?.forEach { file -> traverse(file, srcFile) } - } else { - Logd(TAG, "traverse: $srcFile") - val episode = nameEpisodeMap.remove(relativePath) - if (episode == null) { - Logd(TAG, "traverse: error: episode not exist in map: $relativePath") - filesRemoved.add(relativePath) - srcFile.delete() - return - } - Logd(TAG, "traverse found episode: ${episode.title}") - } - } - - private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) { - val newRunningDownloads: MutableSet = HashSet() - for (url in event.urls) { - if (DownloadServiceInterface.get()?.isDownloadingEpisode(url) == true) newRunningDownloads.add(url) - } - if (newRunningDownloads != runningDownloads) { - runningDownloads = newRunningDownloads - loadItems() - return // Refreshed anyway - } -// for (downloadUrl in event.urls) { -// val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl) -// if (pos >= 0) adapter.notifyItemChangedCompat(pos) -// } - } - - 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.EpisodeEvent -> onEpisodeEvent(event) - is FlowEvent.DownloadsFilterEvent -> onFilterChanged(event) - is FlowEvent.EpisodeMediaEvent -> onEpisodeMediaEvent(event) - is FlowEvent.PlayerSettingsEvent -> loadItems() - is FlowEvent.DownloadLogEvent -> loadItems() - is FlowEvent.QueueEvent -> loadItems() - is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() - is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event) - else -> {} - } - } - } -// if (eventStickySink == null) eventStickySink = lifecycleScope.launch { -// EventFlow.stickyEvents.collectLatest { event -> -// Logd(TAG, "Received sticky event: ${event.TAG}") -// when (event) { -// is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event) -// else -> {} -// } -// } -// } - } - - private fun onFilterChanged(event: FlowEvent.DownloadsFilterEvent) { - val fSet = event.filterValues?.toMutableSet() ?: mutableSetOf() - fSet.add(EpisodeFilter.States.downloaded.name) - prefFilterDownloads = StringUtils.join(fSet, ",") - Logd(TAG, "onFilterChanged: $prefFilterDownloads") - loadItems() - } - - private fun addEmptyView() { - emptyView = EmptyViewHandler(requireContext()) - emptyView.setIcon(R.drawable.ic_download) - emptyView.setTitle(R.string.no_comp_downloads_head_label) - emptyView.setMessage(R.string.no_comp_downloads_label) -// emptyView.attachToRecyclerView(recyclerView) - } - - private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) { -// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}") - var i = 0 - val size: Int = event.episodes.size - while (i < size) { - val item: Episode = event.episodes[i++] - val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id) - if (pos >= 0) { - episodes.removeAt(pos) - val media = item.media - if (media != null && media.downloaded) episodes.add(pos, item) - } - } -// have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash -// if (size > 0) adapter.updateItems(episodes) - refreshInfoBar() - } - - private fun onEpisodeMediaEvent(event: FlowEvent.EpisodeMediaEvent) { -// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}") - var i = 0 - val size: Int = event.episodes.size - while (i < size) { - val item: Episode = event.episodes[i++] - val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id) - if (pos >= 0) { - episodes.removeAt(pos) - val media = item.media - if (media != null && media.downloaded) episodes.add(pos, item) - } - } -// have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash -// if (size > 0) adapter.updateItems(episodes) - refreshInfoBar() - } - - 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()) - } - - private var loadItemsRunning = false - private fun loadItems() { - emptyView.hide() - if (!loadItemsRunning) { - loadItemsRunning = true - lifecycleScope.launch { - try { - withContext(Dispatchers.IO) { - val sortOrder: EpisodeSortOrder? = downloadsSortedOrder - val filter = getFilter() - val downloadedItems = getEpisodes(0, Int.MAX_VALUE, filter, sortOrder) - if (runningDownloads.isEmpty()) { - episodes.clear() - episodes.addAll(downloadedItems) - } else { - val mediaUrls: MutableList = ArrayList() - for (url in runningDownloads) { - if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue - mediaUrls.add(url) - } - val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList() - currentDownloads.addAll(downloadedItems) - episodes.clear() - episodes.addAll(currentDownloads) - } - episodes.retainAll { filter.matchesForQueues(it) } - } - withContext(Dispatchers.Main) { -// adapter.setDummyViews(0) -// adapter.updateItems(episodes) - refreshInfoBar() - } - } catch (e: Throwable) { -// adapter.setDummyViews(0) -// adapter.updateItems(mutableListOf()) - Log.e(TAG, Log.getStackTraceString(e)) - } finally { loadItemsRunning = false } - } - } - } - - private fun getEpisdesWithUrl(urls: List): List { - Logd(TAG, "getEpisdesWithUrl() called ") - if (urls.isEmpty()) return listOf() - val episodes: MutableList = mutableListOf() - for (url in urls) { - val media = realm.query(EpisodeMedia::class).query("downloadUrl == $0", url).first().find() ?: continue - val item_ = media.episodeOrFetch() - if (item_ != null) episodes.add(item_) - } - return realm.copyFromRealm(episodes) - } - - private fun refreshInfoBar() { - var info = String.format(Locale.getDefault(), "%d%s", episodes.size, getString(R.string.episodes_suffix)) - if (episodes.isNotEmpty()) { - var sizeMB: Long = 0 - for (item in episodes) sizeMB += item.media?.size ?: 0 - info += " • " + (sizeMB / 1000000) + " MB" - } - Logd(TAG, "filter value: ${getFilter().values.size} ${getFilter().values.joinToString()}") - if (getFilter().values.size > 1) info += " - ${getString(R.string.filtered_label)}" -// binding.infoBar.text = info - infoBarText.value = info - } - -// override fun onStartSelectMode() { -// swipeActions.detach() -// speedDialView.visibility = View.VISIBLE -// } -// -// override fun onEndSelectMode() { -// speedDialView.close() -// speedDialView.visibility = View.GONE -//// swipeActions.attachTo(recyclerView) -// } - - class DownloadsSortDialog : EpisodeSortDialog() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - sortOrder = downloadsSortedOrder - } - override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) { - if (ascending == EpisodeSortOrder.DATE_OLD_NEW - || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW - || ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW - || ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW - || ascending == EpisodeSortOrder.DURATION_SHORT_LONG - || ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z - || ascending == EpisodeSortOrder.SIZE_SMALL_LARGE - || ascending == EpisodeSortOrder.FEED_TITLE_A_Z) { - super.onAddItem(title, ascending, descending, ascendingIsDefault) - } - } - override fun onSelectionChanged() { - super.onSelectionChanged() - downloadsSortedOrder = sortOrder - EventFlow.postEvent(FlowEvent.DownloadLogEvent()) - } - } - - class DownloadsFilterDialog : EpisodeFilterDialog() { - override fun onFilterChanged(newFilterValues: Set) { - EventFlow.postEvent(FlowEvent.DownloadsFilterEvent(newFilterValues)) - } - companion object { - fun newInstance(filter: EpisodeFilter?): DownloadsFilterDialog { - val dialog = DownloadsFilterDialog() - dialog.filter = filter - dialog.filtersDisabled.add(FeedItemFilterGroup.DOWNLOADED) - dialog.filtersDisabled.add(FeedItemFilterGroup.MEDIA) - return dialog - } - } - } - - companion object { - val TAG = DownloadsCFragment::class.simpleName ?: "Anonymous" - - const val ARG_SHOW_LOGS: String = "show_logs" - private const val KEY_UP_ARROW = "up_arrow" - - // the sort order for the downloads. - var downloadsSortedOrder: EpisodeSortOrder? - get() { - val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + EpisodeSortOrder.DATE_NEW_OLD.code) - return EpisodeSortOrder.fromCodeString(sortOrderStr) - } - set(sortOrder) { - appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + sortOrder!!.code).apply() - } - - var prefFilterDownloads: String - get() = appPrefs.getString(UserPreferences.Prefs.prefDownloadsFilter.name, EpisodeFilter.States.downloaded.name) ?: EpisodeFilter.States.downloaded.name - set(filter) { - appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadsFilter.name, filter).apply() - } - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index 5f56a7e1..5ba5d76c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -1,8 +1,7 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding -import ac.mdiq.podcini.databinding.SimpleListFragmentBinding +import ac.mdiq.podcini.databinding.DownloadsFragmentBinding import ac.mdiq.podcini.net.download.service.DownloadServiceInterface import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs @@ -16,22 +15,20 @@ 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.handler.EpisodeMultiSelectHandler 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.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.EpisodeFilterDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog 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.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -40,15 +37,11 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.widget.Toolbar +import androidx.compose.runtime.* import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SimpleItemAnimator import com.google.android.material.appbar.MaterialToolbar -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 @@ -61,84 +54,63 @@ import java.util.* /** * Displays all completed downloads and provides a button to delete them. */ -@UnstableApi class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { +@UnstableApi class DownloadsFragment : Fragment(), Toolbar.OnMenuItemClickListener { - private var _binding: SimpleListFragmentBinding? = null + private var _binding: DownloadsFragmentBinding? = null private val binding get() = _binding!! private var runningDownloads: Set = HashSet() - private var episodes: MutableList = mutableListOf() + private val episodes = mutableStateListOf() + + private var infoBarText = mutableStateOf("") + private var leftActionState = mutableStateOf(null) + private var rightActionState = mutableStateOf(null) - private lateinit var adapter: DownloadsListAdapter private lateinit var toolbar: MaterialToolbar - private lateinit var recyclerView: EpisodesRecyclerView private lateinit var swipeActions: SwipeActions - private lateinit var speedDialView: SpeedDialView private lateinit var emptyView: EmptyViewHandler private var displayUpArrow = false @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = SimpleListFragmentBinding.inflate(inflater) + _binding = DownloadsFragmentBinding.inflate(inflater) Logd(TAG, "fragment onCreateView") toolbar = binding.toolbar toolbar.setTitle(R.string.downloads_label) toolbar.inflateMenu(R.menu.downloads_completed) 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) (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) - recyclerView = binding.recyclerView - recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) - adapter = DownloadsListAdapter() - adapter.setOnSelectModeListener(this) - recyclerView.adapter = adapter - recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) - - swipeActions = SwipeActions(this, TAG).attachTo(recyclerView) - lifecycle.addObserver(swipeActions) + swipeActions = SwipeActions(this, TAG) swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name)) - refreshSwipeTelltale() - binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() } - binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() } - - val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator - if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false - - binding.progLoading.visibility = View.VISIBLE - - val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root) - speedDialView = multiSelectDial.fabSD - speedDialView.overlayLayout = multiSelectDial.fabSDOverlay - speedDialView.inflate(R.menu.episodes_apply_action_speeddial) - speedDialView.removeActionItemById(R.id.download_batch) - speedDialView.removeActionItemById(R.id.remove_from_queue_batch) - speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener { - override fun onMainActionSelected(): Boolean { - return false + binding.infobar.setContent { + CustomTheme(requireContext()) { + InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) } - 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 -> - adapter.selectedItems.let { - EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(it) + } + + binding.lazyColumn.setContent { + CustomTheme(requireContext()) { + EpisodeLazyColumn(activity as MainActivity, episodes = episodes, + 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())}, + actionButton_ = { DeleteActionButton(it) } ) } - adapter.endSelectMode() - true } +// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) +// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) + + lifecycle.addObserver(swipeActions) + refreshSwipeTelltale() if (arguments != null && requireArguments().getBoolean(ARG_SHOW_LOGS, false)) DownloadLogFragment().show(childFragmentManager, null) addEmptyView() @@ -154,13 +126,12 @@ import java.util.* override fun onStop() { super.onStop() cancelFlowEvents() - val recyclerView = binding.recyclerView - val childCount = recyclerView.childCount - for (i in 0 until childCount) { - val child = recyclerView.getChildAt(i) - val viewHolder = recyclerView.getChildViewHolder(child) as? EpisodeViewHolder - viewHolder?.stopDBMonitor() - } +// 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 onSaveInstanceState(outState: Bundle) { @@ -171,12 +142,11 @@ import java.util.* override fun onDestroyView() { Logd(TAG, "onDestroyView") _binding = null - adapter.endSelectMode() - adapter.clearData() +// adapter.endSelectMode() +// adapter.clearData() toolbar.setOnMenuItemClickListener(null) toolbar.setOnLongClickListener(null) - episodes = mutableListOf() - + episodes.clear() super.onDestroyView() } @@ -258,10 +228,10 @@ import java.util.* loadItems() return // Refreshed anyway } - for (downloadUrl in event.urls) { - val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl) - if (pos >= 0) adapter.notifyItemChangedCompat(pos) - } +// for (downloadUrl in event.urls) { +// val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl) +// if (pos >= 0) adapter.notifyItemChangedCompat(pos) +// } } private var eventSink: Job? = null @@ -313,7 +283,6 @@ import java.util.* emptyView.setIcon(R.drawable.ic_download) emptyView.setTitle(R.string.no_comp_downloads_head_label) emptyView.setMessage(R.string.no_comp_downloads_label) - emptyView.attachToRecyclerView(recyclerView) } private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) { @@ -330,7 +299,7 @@ import java.util.* } } // have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash - if (size > 0) adapter.updateItems(episodes) +// if (size > 0) adapter.updateItems(episodes) refreshInfoBar() } @@ -348,13 +317,13 @@ import java.util.* } } // have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash - if (size > 0) adapter.updateItems(episodes) +// if (size > 0) adapter.updateItems(episodes) refreshInfoBar() } 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 } private var loadItemsRunning = false @@ -367,9 +336,11 @@ import java.util.* withContext(Dispatchers.IO) { val sortOrder: EpisodeSortOrder? = downloadsSortedOrder val filter = getFilter() - val downloadedItems = getEpisodes(0, Int.MAX_VALUE, filter, sortOrder) - if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList() - else { + val downloadedItems = getEpisodes(0, Int.MAX_VALUE, filter, sortOrder, false) + if (runningDownloads.isEmpty()) { + episodes.clear() + episodes.addAll(downloadedItems) + } else { val mediaUrls: MutableList = ArrayList() for (url in runningDownloads) { if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue @@ -377,31 +348,23 @@ import java.util.* } val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList() currentDownloads.addAll(downloadedItems) - episodes = currentDownloads + episodes.clear() + episodes.addAll(currentDownloads) } - episodes = episodes.filter { filter.matchesForQueues(it) }.toMutableList() - } - withContext(Dispatchers.Main) { -// adapter.setDummyViews(0) - binding.progLoading.visibility = View.GONE - adapter.updateItems(episodes) - refreshInfoBar() + episodes.retainAll { filter.matchesForQueues(it) } } - } catch (e: Throwable) { -// adapter.setDummyViews(0) - adapter.updateItems(mutableListOf()) - Log.e(TAG, Log.getStackTraceString(e)) + withContext(Dispatchers.Main) { refreshInfoBar() } + } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } finally { loadItemsRunning = false } } } } - private fun getEpisdesWithUrl(urls: List?): List { + private fun getEpisdesWithUrl(urls: List): List { Logd(TAG, "getEpisdesWithUrl() called ") - if (urls == null) return listOf() + if (urls.isEmpty()) return listOf() val episodes: MutableList = mutableListOf() for (url in urls) { - if (url == null) continue val media = realm.query(EpisodeMedia::class).query("downloadUrl == $0", url).first().find() ?: continue val item_ = media.episodeOrFetch() if (item_ != null) episodes.add(item_) @@ -416,31 +379,9 @@ import java.util.* for (item in episodes) sizeMB += item.media?.size ?: 0 info += " • " + (sizeMB / 1000000) + " MB" } + Logd(TAG, "filter value: ${getFilter().values.size} ${getFilter().values.joinToString()}") if (getFilter().values.size > 1) info += " - ${getString(R.string.filtered_label)}" - binding.infoBar.text = info - } - - override fun onStartSelectMode() { - swipeActions.detach() - speedDialView.visibility = View.VISIBLE - } - - override fun onEndSelectMode() { - speedDialView.close() - speedDialView.visibility = View.GONE - swipeActions.attachTo(recyclerView) - } - - @UnstableApi private inner class DownloadsListAdapter : EpisodesAdapter(activity as MainActivity) { - @UnstableApi override fun afterBindViewHolder(holder: EpisodeViewHolder, pos: Int) { - if (holder.episode != null && !inActionMode()) { - if (holder.episode!!.isDownloaded) { - val item = getItem(pos) ?: return - val actionButton = DeleteActionButton(item) - actionButton.configure(holder.secondaryActionButton, holder.secondaryActionIcon, requireContext()) - } - } - } + infoBarText.value = info } class DownloadsSortDialog : EpisodeSortDialog() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index 23fd5ab5..4fcefab0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -1,9 +1,9 @@ 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.databinding.MultiSelectSpeedDialBinding import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.storage.database.Feeds.getFeed @@ -16,41 +16,37 @@ 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.handler.EpisodeMultiSelectHandler +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.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.FeedEpisodesHeader +import ac.mdiq.podcini.ui.compose.InforBar import ac.mdiq.podcini.ui.dialog.* -import ac.mdiq.podcini.ui.utils.ToolbarIconTintManager import ac.mdiq.podcini.ui.utils.TransitionEffect -import ac.mdiq.podcini.ui.view.EpisodeViewHolder -import ac.mdiq.podcini.util.IntentUtils -import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.ShareUtils -import ac.mdiq.podcini.util.EventFlow -import ac.mdiq.podcini.util.FlowEvent +import ac.mdiq.podcini.util.* import android.app.Activity import android.content.Context import android.content.res.Configuration -import android.graphics.Color import android.os.Bundle import android.speech.tts.TextToSpeech import android.util.Log import android.view.* import android.widget.Toast import androidx.annotation.OptIn -import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.Toolbar +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import androidx.recyclerview.widget.RecyclerView -import coil.load import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.snackbar.Snackbar -import com.leinardi.android.speeddial.SpeedDialActionItem -import com.leinardi.android.speeddial.SpeedDialView import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest import org.apache.commons.lang3.StringUtils @@ -60,26 +56,27 @@ import java.util.concurrent.Semaphore /** * Displays a list of FeedItems. */ -@UnstableApi class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener { +@UnstableApi class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var _binding: FeedItemListFragmentBinding? = null private val binding get() = _binding!! - private var _dialBinding: MultiSelectSpeedDialBinding? = null - private val dialBinding get() = _dialBinding!! - private lateinit var adapter: EpisodesAdapter private lateinit var swipeActions: SwipeActions - private lateinit var nextPageLoader: MoreContentListFooterUtil + + private var infoBarText = mutableStateOf("") + private var leftActionState = mutableStateOf(null) + private var rightActionState = mutableStateOf(null) private var infoTextFiltered = "" private var infoTextUpdate = "" private var displayUpArrow = false private var headerCreated = false private var feedID: Long = 0 - private var feed: Feed? = null - private var episodes: MutableList = mutableListOf() + private var feed by mutableStateOf(null) + private val episodes = mutableStateListOf() private var enableFilter: Boolean = true + private var filterButColor = mutableStateOf(Color.White) private val ioScope = CoroutineScope(Dispatchers.IO) private var onInit: Boolean = true @@ -95,93 +92,86 @@ import java.util.concurrent.Semaphore Logd(TAG, "fragment onCreateView") _binding = FeedItemListFragmentBinding.inflate(inflater) - _dialBinding = MultiSelectSpeedDialBinding.bind(binding.root) binding.toolbar.inflateMenu(R.menu.feed_episodes) binding.toolbar.setOnMenuItemClickListener(this) - binding.toolbar.setOnLongClickListener { - binding.recyclerView.scrollToPosition(5) - binding.recyclerView.post { binding.recyclerView.smoothScrollToPosition(0) } - binding.appBar.setExpanded(true) - false - } +// binding.toolbar.setOnLongClickListener { +// binding.recyclerView.scrollToPosition(5) +// binding.recyclerView.post { binding.recyclerView.smoothScrollToPosition(0) } +// binding.appBar.setExpanded(true) +// false +// } displayUpArrow = parentFragmentManager.backStackEntryCount != 0 if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) (activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow) updateToolbar() - binding.recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) - adapter = EpisodesAdapter(activity as MainActivity) { pos, episode -> - Logd(TAG, "FeedEpisode refreshPosCallback: $pos ${episode.title}") -// if (pos >= 0 && pos < episodes.size) episodes[pos] = episode - redoFilter() -// adapter.notifyDataSetChanged() - } - adapter.setOnSelectModeListener(this) - binding.recyclerView.adapter = adapter - - swipeActions = SwipeActions(this, TAG).attachTo(binding.recyclerView) - lifecycle.addObserver(swipeActions) - refreshSwipeTelltale() - binding.header.leftActionIcon.setOnClickListener { swipeActions.showDialog() } - binding.header.rightActionIcon.setOnClickListener { swipeActions.showDialog() } - - binding.progressBar.visibility = View.VISIBLE - - val iconTintManager: ToolbarIconTintManager = object : ToolbarIconTintManager( - requireContext(), binding.toolbar, binding.collapsingToolbar) { - override fun doTint(themedContext: Context) { - binding.toolbar.menu.findItem(R.id.refresh_feed).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh)) - binding.toolbar.menu.findItem(R.id.action_search).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search)) + swipeActions = SwipeActions(this, TAG) + fun filterClick() { + if (enableFilter && feed != null) { + val dialog = FeedEpisodeFilterDialog(feed) + dialog.filter = feed!!.episodeFilter + dialog.show(childFragmentManager, null) } } - iconTintManager.updateTint() - binding.appBar.addOnOffsetChangedListener(iconTintManager) - - nextPageLoader = MoreContentListFooterUtil(binding.moreContent.moreContentListFooter) - nextPageLoader.setClickListener(object : MoreContentListFooterUtil.Listener { - override fun onClick() { - if (feed != null) FeedUpdateManager.runOnce(requireContext(), feed, true) + fun filterLongClick() { + if (feed != null) { + enableFilter = !enableFilter + waitForLoading() + loadItemsRunning = true + val etmp = mutableListOf() + if (enableFilter) { + filterButColor.value = Color.White + val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) } + etmp.addAll(episodes_) + } else { + filterButColor.value = Color.Red + etmp.addAll(feed!!.episodes) + } + val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0) + if (sortOrder != null) getPermutor(sortOrder).reorder(etmp) + episodes.clear() + episodes.addAll(etmp) + loadItemsRunning = false } - }) - binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(view: RecyclerView, deltaX: Int, deltaY: Int) { - super.onScrolled(view, deltaX, deltaY) - val hasMorePages = feed != null && feed!!.isPaged && feed!!.nextPageLink != null - val pageLoaderVisible = binding.recyclerView.isScrolledToBottom && hasMorePages - nextPageLoader.root.visibility = if (pageLoaderVisible) View.VISIBLE else View.GONE - binding.recyclerView.setPadding(binding.recyclerView.paddingLeft, 0, binding.recyclerView.paddingRight, - if (pageLoaderVisible) nextPageLoader.root.measuredHeight else 0) + } + binding.header.setContent { + CustomTheme(requireContext()) { + FeedEpisodesHeader(activity = (activity as MainActivity), feed = feed, filterButColor = filterButColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()}) } - }) - - binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance)) - binding.swipeRefresh.setProgressViewEndTarget(false, 0) - binding.swipeRefresh.setOnRefreshListener { - FeedUpdateManager.runOnceOrAsk(requireContext(), feed) } - - // Init action UI (via a FAB Speed Dial) - dialBinding.fabSD.overlayLayout = dialBinding.fabSDOverlay - dialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial) - dialBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener { - override fun onMainActionSelected(): Boolean { - return false + binding.infobar.setContent { + CustomTheme(requireContext()) { + InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) } - override fun onToggleChanged(open: Boolean) { - if (open && adapter.selectedCount == 0) { - (activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT) - dialBinding.fabSD.close() - } + } + binding.lazyColumn.setContent { + CustomTheme(requireContext()) { + EpisodeLazyColumn(activity as MainActivity, episodes = episodes, + 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())}, ) } - }) - dialBinding.fabSD.setOnActionSelectedListener { actionItem: SpeedDialActionItem -> - EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(adapter.selectedItems) - adapter.endSelectMode() - true } -// loadItems() + + lifecycle.addObserver(swipeActions) + refreshSwipeTelltale() + +// val iconTintManager: ToolbarIconTintManager = object : ToolbarIconTintManager(requireContext(), binding.toolbar, binding.collapsingToolbar) { +// override fun doTint(themedContext: Context) { +// binding.toolbar.menu.findItem(R.id.refresh_feed).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh)) +// binding.toolbar.menu.findItem(R.id.action_search).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search)) +// } +// } +// iconTintManager.updateTint() +// binding.appBar.addOnOffsetChangedListener(iconTintManager) + +// binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance)) +// binding.swipeRefresh.setProgressViewEndTarget(false, 0) +// binding.swipeRefresh.setOnRefreshListener { +// FeedUpdateManager.runOnceOrAsk(requireContext(), feed) +// } + return binding.root } @@ -196,15 +186,7 @@ import java.util.concurrent.Semaphore override fun onStop() { Logd(TAG, "onStop() called") super.onStop() -// adapter.refreshFragPosCallback = null cancelFlowEvents() - val recyclerView = binding.recyclerView - val childCount = recyclerView.childCount - for (i in 0 until childCount) { - val child = recyclerView.getChildAt(i) - val viewHolder = recyclerView.getChildViewHolder(child) as? EpisodeViewHolder - viewHolder?.stopDBMonitor() - } } private val semaphore = Semaphore(0) @@ -231,14 +213,11 @@ import java.util.concurrent.Semaphore binding.toolbar.setOnMenuItemClickListener(null) binding.toolbar.setOnLongClickListener(null) _binding = null - _dialBinding = null ioScope.cancel() - adapter.endSelectMode() - adapter.clearData() feed = null - episodes = mutableListOf() + episodes.clear() tts?.stop() tts?.shutdown() @@ -274,8 +253,8 @@ import java.util.concurrent.Semaphore override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val horizontalSpacing = resources.getDimension(R.dimen.additional_horizontal_spacing).toInt() - binding.header.headerContainer.setPadding(horizontalSpacing, binding.header.headerContainer.paddingTop, - horizontalSpacing, binding.header.headerContainer.paddingBottom) +// binding.header.headerContainer.setPadding(horizontalSpacing, binding.header.headerContainer.paddingTop, +// horizontalSpacing, binding.header.headerContainer.paddingBottom) } @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { @@ -329,7 +308,7 @@ import java.util.concurrent.Semaphore while (i < size) { val item = event.episodes[i++] if (item.feedId != feed!!.id) continue - adapter.notifyDataSetChanged() +// adapter.notifyDataSetChanged() break } } @@ -339,8 +318,8 @@ import java.util.concurrent.Semaphore if (feed != null) { val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, event.episode.id) if (pos >= 0) { - if (filterOutEpisode(event.episode)) adapter.updateItems(episodes) - else adapter.notifyItemChangedCompat(pos) +// if (filterOutEpisode(event.episode)) adapter.updateItems(episodes) +// else adapter.notifyItemChangedCompat(pos) } } } @@ -353,7 +332,7 @@ import java.util.concurrent.Semaphore val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl) if (pos >= 0) { Logd(TAG, "onEpisodeDownloadEvent $pos ${episodes[pos].title}") - adapter.notifyItemChangedCompat(pos) +// adapter.notifyItemChangedCompat(pos) } } } @@ -402,37 +381,17 @@ import java.util.concurrent.Semaphore } } - override fun onStartSelectMode() { - swipeActions.detach() - if (feed != null && feed!!.isLocalFeed) dialBinding.fabSD.removeActionItemById(R.id.download_batch) - - if (feed?.link?.contains("youtube.com") == true) { - dialBinding.fabSD.removeActionItemById(R.id.download_batch) - dialBinding.fabSD.removeActionItemById(R.id.delete_batch) - dialBinding.fabSD.removeActionItemById(R.id.add_to_queue_batch) - dialBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch) - } - dialBinding.fabSD.visibility = View.VISIBLE - updateToolbar() - } - - override fun onEndSelectMode() { - dialBinding.fabSD.close() - dialBinding.fabSD.visibility = View.GONE - swipeActions.attachTo(binding.recyclerView) - } - private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdatingEvent) { - nextPageLoader.setLoadingState(event.isRunning) - if (!event.isRunning) nextPageLoader.root.visibility = View.GONE +// nextPageLoader.setLoadingState(event.isRunning) +// if (!event.isRunning) nextPageLoader.root.visibility = View.GONE infoTextUpdate = if (event.isRunning) getString(R.string.refreshing_label) else "" - binding.header.txtvInformation.text = ("{gmo-info} $infoTextFiltered $infoTextUpdate") - binding.swipeRefresh.isRefreshing = event.isRunning + infoBarText.value = "$infoTextFiltered $infoTextUpdate" +// binding.swipeRefresh.isRefreshing = event.isRunning } private fun refreshSwipeTelltale() { - if (swipeActions.actions?.left != null) binding.header.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon()) - if (swipeActions.actions?.right != null) binding.header.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon()) + leftActionState.value = swipeActions.actions?.left + rightActionState.value = swipeActions.actions?.right } @UnstableApi private fun refreshHeaderView() { @@ -441,75 +400,37 @@ import java.util.concurrent.Semaphore Log.e(TAG, "Unable to refresh header view") return } - loadFeedImage() - if (feed!!.lastUpdateFailed) binding.header.txtvFailure.visibility = View.VISIBLE - else binding.header.txtvFailure.visibility = View.GONE - - if (feed!!.preferences != null && !feed!!.preferences!!.keepUpdated) { - binding.header.txtvUpdatesDisabled.text = ("{gmo-pause_circle_outline} ${this.getString(R.string.updates_disabled_label)}") - binding.header.txtvUpdatesDisabled.visibility = View.VISIBLE - } else binding.header.txtvUpdatesDisabled.visibility = View.GONE - - binding.header.txtvTitle.text = feed!!.title - binding.header.txtvAuthor.text = feed!!.author - binding.header.txtvInformation.setOnClickListener {} +// loadFeedImage() +// if (feed!!.lastUpdateFailed) binding.header.txtvFailure.visibility = View.VISIBLE +// else binding.header.txtvFailure.visibility = View.GONE + +// if (feed!!.preferences != null && !feed!!.preferences!!.keepUpdated) { +// binding.header.txtvUpdatesDisabled.text = ("{gmo-pause_circle_outline} ${this.getString(R.string.updates_disabled_label)}") +// binding.header.txtvUpdatesDisabled.visibility = View.VISIBLE +// } else binding.header.txtvUpdatesDisabled.visibility = View.GONE + infoTextFiltered = "" if (!feed?.preferences?.filterString.isNullOrEmpty()) { val filter: EpisodeFilter = feed!!.episodeFilter if (filter.values.isNotEmpty()) { infoTextFiltered = this.getString(R.string.filtered_label) - binding.header.txtvInformation.setOnClickListener { - val dialog = FeedEpisodeFilterDialog(feed) - dialog.filter = feed!!.episodeFilter - dialog.show(childFragmentManager, null) - } +// binding.header.txtvInformation.setOnClickListener { +// val dialog = FeedEpisodeFilterDialog(feed) +// dialog.filter = feed!!.episodeFilter +// dialog.show(childFragmentManager, null) +// } } } - binding.header.txtvInformation.text = ("{gmo-info} $infoTextFiltered $infoTextUpdate") + infoBarText.value = "$infoTextFiltered $infoTextUpdate" } @UnstableApi private fun setupHeaderView() { if (feed == null || headerCreated) return // binding.imgvBackground.colorFilter = LightingColorFilter(-0x99999a, 0x000000) - binding.header.imgvCover.setOnClickListener { showFeedInfo() } - binding.header.butShowSettings.setOnClickListener { - if (feed != null) { - val fragment = FeedSettingsFragment.newInstance(feed!!) - (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE) - } - } - binding.header.butFilter.setOnClickListener { - if (enableFilter && feed != null) { - val dialog = FeedEpisodeFilterDialog(feed) - dialog.filter = feed!!.episodeFilter - dialog.show(childFragmentManager, null) - } - } - binding.header.butFilter.setOnLongClickListener { - if (feed != null) { - enableFilter = !enableFilter - waitForLoading() - loadItemsRunning = true - episodes.clear() - if (enableFilter) { - binding.header.butFilter.setColorFilter(Color.WHITE) - val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) } - episodes.addAll(episodes_) - } else { - binding.header.butFilter.setColorFilter(Color.RED) - episodes.addAll(feed!!.episodes) - } - val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0) - if (sortOrder != null) getPermutor(sortOrder).reorder(episodes) - binding.header.counts.text = episodes.size.toString() - adapter.updateItems(episodes, feed) - loadItemsRunning = false - } - true - } - binding.header.txtvFailure.setOnClickListener { showErrorDetails() } - binding.header.counts.text = adapter.itemCount.toString() +// binding.header.imgvCover.setOnClickListener { showFeedInfo() } + +// binding.header.txtvFailure.setOnClickListener { showErrorDetails() } headerCreated = true } @@ -535,15 +456,6 @@ import java.util.concurrent.Semaphore } } - private fun loadFeedImage() { - if (!feed?.imageUrl.isNullOrBlank()) { - binding.header.imgvCover.load(feed!!.imageUrl) { - placeholder(R.color.light_gray) - error(R.mipmap.ic_launcher) - } - } else binding.header.imgvCover.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_launcher_foreground)) - } - private var loadItemsRunning = false private fun waitForLoading() { while (loadItemsRunning) Thread.sleep(50) @@ -569,20 +481,21 @@ import java.util.concurrent.Semaphore private fun loadFeed() { if (!loadItemsRunning) { loadItemsRunning = true - adapter.updateItems(mutableListOf()) lifecycleScope.launch { try { feed = withContext(Dispatchers.IO) { val feed_ = getFeed(feedID) if (feed_ != null) { Logd(TAG, "loadItems feed_.episodes.size: ${feed_.episodes.size}") - episodes.clear() + val etmp = mutableListOf() if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) { val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) } - episodes.addAll(episodes_) - } else episodes.addAll(feed_.episodes) + etmp.addAll(episodes_) + } else etmp.addAll(feed_.episodes) val sortOrder = feed_.sortOrder - if (sortOrder != null) getPermutor(sortOrder).reorder(episodes) + if (sortOrder != null) getPermutor(sortOrder).reorder(etmp) + episodes.clear() + episodes.addAll(etmp) if (onInit) { var hasNonMediaItems = false for (item in episodes) { @@ -610,26 +523,19 @@ import java.util.concurrent.Semaphore Logd(TAG, "loadItems subscribe called ${feed?.title}") swipeActions.setFilter(feed?.episodeFilter) refreshHeaderView() - binding.progressBar.visibility = View.GONE -// adapter.setDummyViews(0) - if (feed != null) { - adapter.updateItems(episodes, feed) - binding.header.counts.text = episodes.size.toString() - } +// if (feed != null) { +// adapter.updateItems(episodes, feed) +// binding.header.counts.text = episodes.size.toString() +// } updateToolbar() } } catch (e: Throwable) { feed = null refreshHeaderView() -// adapter.setDummyViews(0) - adapter.updateItems(mutableListOf()) updateToolbar() Log.e(TAG, Log.getStackTraceString(e)) - } catch (e: Exception) { - Log.e(TAG, Log.getStackTraceString(e)) - } finally { - loadItemsRunning = false - } + } catch (e: Exception) { Log.e(TAG, Log.getStackTraceString(e)) + } finally { loadItemsRunning = false } } } } @@ -637,8 +543,8 @@ import java.util.concurrent.Semaphore private fun onKeyUp(event: KeyEvent) { if (!isAdded || !isVisible || !isMenuVisible) return when (event.keyCode) { - KeyEvent.KEYCODE_T -> binding.recyclerView.smoothScrollToPosition(0) - KeyEvent.KEYCODE_B -> binding.recyclerView.smoothScrollToPosition(adapter.itemCount - 1) +// KeyEvent.KEYCODE_T -> binding.recyclerView.smoothScrollToPosition(0) +// KeyEvent.KEYCODE_B -> binding.recyclerView.smoothScrollToPosition(adapter.itemCount - 1) else -> {} } } @@ -684,39 +590,6 @@ import java.util.concurrent.Semaphore } } - /** - * Utility methods for the more_content_list_footer layout. - */ - private class MoreContentListFooterUtil(val root: View) { - private var loading = false - private var listener: Listener? = null - - init { - root.setOnClickListener { - if (!loading) listener?.onClick() - } - } - fun setLoadingState(newState: Boolean) { - val binding = MoreContentListFooterBinding.bind(root) - val imageView = binding.imgExpand - val progressBar = binding.progBar - if (newState) { - imageView.visibility = View.GONE - progressBar.visibility = View.VISIBLE - } else { - imageView.visibility = View.VISIBLE - progressBar.visibility = View.GONE - } - loading = newState - } - fun setClickListener(l: Listener?) { - listener = l - } - interface Listener { - fun onClick() - } - } - companion object { val TAG = FeedEpisodesFragment::class.simpleName ?: "Anonymous" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt index 73d5074b..b46c4db5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt @@ -7,16 +7,12 @@ import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor -import ac.mdiq.podcini.ui.activity.MainActivity -import ac.mdiq.podcini.ui.adapter.EpisodesAdapter import ac.mdiq.podcini.ui.dialog.ConfirmationDialog import ac.mdiq.podcini.ui.dialog.DatesFilterDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog -import ac.mdiq.podcini.ui.view.EpisodeViewHolder -import ac.mdiq.podcini.util.MiscFormatter -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.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater @@ -58,19 +54,19 @@ import kotlin.math.min } override fun createListAdaptor() { - adapter = object : EpisodesAdapter(activity as MainActivity) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder { - return object: EpisodeViewHolder(mainActivityRef.get()!!, parent) { - override fun setPubDate(item: Episode) { - val playDate = Date(item.media?.getLastPlayedTime()?:0L) - pubDate.text = MiscFormatter.formatAbbrev(activity, playDate) - pubDate.setContentDescription(MiscFormatter.formatForAccessibility(playDate)) - } - } - } - } - adapter.setOnSelectModeListener(this) - recyclerView.adapter = adapter +// adapter = object : EpisodesAdapter(activity as MainActivity) { +// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder { +// return object: EpisodeViewHolder(mainActivityRef.get()!!, parent) { +// override fun setPubDate(item: Episode) { +// val playDate = Date(item.media?.getLastPlayedTime()?:0L) +// pubDate.text = MiscFormatter.formatAbbrev(activity, playDate) +// pubDate.setContentDescription(MiscFormatter.formatForAccessibility(playDate)) +// } +// } +// } +// } +// adapter.setOnSelectModeListener(this) +// recyclerView.adapter = adapter } override fun onStart() { @@ -85,7 +81,6 @@ import kotlin.math.min override fun onDestroyView() { allHistory = listOf() - adapter.clearData() super.onDestroyView() } @@ -128,14 +123,12 @@ import kotlin.math.min toolbar.menu.findItem(R.id.clear_history_item).setVisible(episodes.isNotEmpty()) swipeActions.setFilter(getFilter()) - txtvInformation.visibility = View.VISIBLE + var info = "${episodes.size} episodes" if (getFilter().values.isNotEmpty()) { - txtvInformation.text = "${adapter.totalNumberOfItems} episodes - ${getString(R.string.filtered_label)}" + info += " - ${getString(R.string.filtered_label)}" emptyView.setMessage(R.string.no_all_episodes_filtered_label) - } else { - txtvInformation.text = "${adapter.totalNumberOfItems} episodes" - emptyView.setMessage(R.string.no_all_episodes_label) - } + } else emptyView.setMessage(R.string.no_all_episodes_label) + infoBarText.value = info toolbar.menu?.findItem(R.id.action_favorites)?.setIcon(if (getFilter().showIsFavorite) R.drawable.ic_star else R.drawable.ic_star_border) } @@ -171,14 +164,7 @@ import kotlin.math.min loadItemsRunning = false } if (allHistory.isEmpty()) return listOf() - return allHistory.subList(0, min(allHistory.size, page * EPISODES_PER_PAGE)) - } - - override fun loadMoreData(page: Int): List { - val offset = (page - 1) * EPISODES_PER_PAGE - if (offset >= allHistory.size) return listOf() - val toIndex = offset + EPISODES_PER_PAGE - return allHistory.subList(offset, min(allHistory.size, toIndex)) + return allHistory } override fun loadTotalItemCount(): Int { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index 86e415e1..2fd22a27 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -141,9 +141,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { datasetStats = result navAdapter.notifyDataSetChanged() } - } catch (e: Throwable) { - Log.e(TAG, Log.getStackTraceString(e)) - } + } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } } @@ -183,7 +181,6 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { QueuesFragment.TAG -> R.drawable.ic_playlist_play AllEpisodesFragment.TAG -> R.drawable.ic_feed DownloadsFragment.TAG -> R.drawable.ic_download - DownloadsCFragment.TAG -> R.drawable.ic_download HistoryFragment.TAG -> R.drawable.ic_history SubscriptionsFragment.TAG -> R.drawable.ic_subscriptions StatisticsFragment.TAG -> R.drawable.ic_chart_box @@ -325,29 +322,6 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { } } } - DownloadsCFragment.TAG -> { - val epCacheSize = episodeCacheSize - // don't count episodes that can be reclaimed - val spaceUsed = ((datasetStats?.numDownloaded ?: 0) - (datasetStats?.numReclaimables ?: 0)) - holder.count.text = NumberFormat.getInstance().format(spaceUsed.toLong()) - holder.count.visibility = View.VISIBLE - if (epCacheSize in 1..spaceUsed) { - holder.count.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_disc_alert, 0) - holder.count.visibility = View.VISIBLE - holder.count.setOnClickListener { - MaterialAlertDialogBuilder(context) - .setTitle(R.string.episode_cache_full_title) - .setMessage(R.string.episode_cache_full_message) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.open_autodownload_settings) { _: DialogInterface?, _: Int -> - val intent = Intent(context, PreferenceActivity::class.java) - intent.putExtra(PreferenceActivity.OPEN_AUTO_DOWNLOAD_SETTINGS, true) - context.startActivity(intent) - } - .show() - } - } - } HistoryFragment.TAG -> { val historyCount = datasetStats?.historyCount ?: 0 if (historyCount > 0) { @@ -406,7 +380,6 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { QueuesFragment.TAG, AllEpisodesFragment.TAG, DownloadsFragment.TAG, - DownloadsCFragment.TAG, HistoryFragment.TAG, StatisticsFragment.TAG, OnlineSearchFragment.TAG, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt index ad02ad14..a2e742ab 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt @@ -75,7 +75,6 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL import kotlin.concurrent.Volatile -import kotlin.math.min /** * Downloads a feed from a feed URL and parses it. Subclasses can display the @@ -828,7 +827,7 @@ class OnlineFeedFragment : Fragment() { toolbar.inflateMenu(R.menu.episodes) toolbar.setTitle(R.string.episodes_label) updateToolbar() - adapter.setOnSelectModeListener(null) +// adapter.setOnSelectModeListener(null) return root } override fun onStart() { @@ -849,13 +848,7 @@ class OnlineFeedFragment : Fragment() { } override fun loadData(): List { if (episodeList.isEmpty()) return listOf() - return episodeList.subList(0, min(episodeList.size, page * EPISODES_PER_PAGE)) - } - override fun loadMoreData(page: Int): List { - val offset = (page - 1) * EPISODES_PER_PAGE - if (offset >= episodeList.size) return listOf() - val toIndex = offset + EPISODES_PER_PAGE - return episodeList.subList(offset, min(episodeList.size, toIndex)) + return episodeList } override fun loadTotalItemCount(): Int { return episodeList.size @@ -869,6 +862,7 @@ class OnlineFeedFragment : Fragment() { binding.toolbar.menu.findItem(R.id.action_search).setVisible(false) binding.toolbar.menu.findItem(R.id.action_favorites).setVisible(false) binding.toolbar.menu.findItem(R.id.filter_items).setVisible(false) + infoBarText.value = "${episodes.size} episodes" } @OptIn(UnstableApi::class) override fun onMenuItemClick(item: MenuItem): Boolean { if (super.onOptionsItemSelected(item)) return true @@ -902,7 +896,6 @@ class OnlineFeedFragment : Fragment() { i.setEpisodes(episodes) return i } - } } diff --git a/app/src/main/res/layout/base_episodes_list_fragment.xml b/app/src/main/res/layout/base_episodes_list_fragment.xml index 346ca605..77a5d2e7 100644 --- a/app/src/main/res/layout/base_episodes_list_fragment.xml +++ b/app/src/main/res/layout/base_episodes_list_fragment.xml @@ -1,10 +1,11 @@ - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content"/> - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content"/> - + diff --git a/app/src/main/res/layout/downloads_fragment.xml b/app/src/main/res/layout/downloads_fragment.xml index 1abe89d9..caffc368 100644 --- a/app/src/main/res/layout/downloads_fragment.xml +++ b/app/src/main/res/layout/downloads_fragment.xml @@ -32,7 +32,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"/> - + + diff --git a/app/src/main/res/layout/feed_item_list_fragment.xml b/app/src/main/res/layout/feed_item_list_fragment.xml index 2d914f80..d5e779ec 100644 --- a/app/src/main/res/layout/feed_item_list_fragment.xml +++ b/app/src/main/res/layout/feed_item_list_fragment.xml @@ -1,82 +1,40 @@ - + android:orientation="vertical"> + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> - - - - - - - - - - - - - - - - - - + android:minHeight="?android:attr/actionBarSize" + app:navigationIcon="?homeAsUpIndicator" + app:navigationContentDescription="@string/toolbar_back_button_content_description"/> - - - + android:layout_height="wrap_content"/> - + - + - - - + android:layout_height="wrap_content"/> - + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index cf2975da..07e1584c 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -140,7 +140,6 @@ @string/queue_label @string/episodes_label @string/downloads_label - @string/downloadsC_label @string/playback_history_label @string/statistics_label @string/add_feed_label @@ -168,7 +167,6 @@ QueuesFragment AllEpisodesFragment DownloadsFragment - DownloadsCFragment PlaybackHistoryFragment AddFeedFragment StatisticsFragment @@ -180,7 +178,6 @@ @string/queue_label @string/episodes_label @string/downloads_label - @string/downloadsC_label @string/playback_history_label @string/add_feed_label @string/statistics_label diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54b6dc3a..28f5066b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,6 @@ Favorites Settings Downloads - DownloadsC Open settings Download log Subscriptions diff --git a/changelog.md b/changelog.md index 822d4aaf..5942feb8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +# 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 + # 6.7.3 * fixed bug in nextcloud auth: thanks to Dacid99's PR diff --git a/fastlane/metadata/android/en-US/changelogs/3020257.txt b/fastlane/metadata/android/en-US/changelogs/3020257.txt new file mode 100644 index 00000000..188bd073 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020257.txt @@ -0,0 +1,4 @@ + Version 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