diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt index 577d5ac40..d51d86552 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt @@ -8,6 +8,8 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -26,16 +28,16 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Archive import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Unarchive -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberDismissState import androidx.compose.material.rememberScaffoldState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -52,6 +54,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -70,6 +73,7 @@ import app.omnivore.omnivore.feature.save.SaveState import app.omnivore.omnivore.feature.save.SaveViewModel import app.omnivore.omnivore.feature.savedItemViews.SavedItemCard import app.omnivore.omnivore.navigation.Routes +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -141,8 +145,7 @@ internal fun LibraryView( is LibraryUiState.Success -> { LibraryViewContent( viewModel, - modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()), + paddingValues = paddingValues, uiState = uiState ) } @@ -293,152 +296,155 @@ fun EditBottomSheet( } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable fun LibraryViewContent( libraryViewModel: LibraryViewModel, - modifier: Modifier, + paddingValues: PaddingValues, uiState: LibraryUiState ) { val context = LocalContext.current val listState = rememberLazyListState() - val pullRefreshState = rememberPullRefreshState( - refreshing = libraryViewModel.isRefreshing, - onRefresh = { libraryViewModel.refresh() } - ) + val pullToRefreshState = rememberPullToRefreshState() + if (pullToRefreshState.isRefreshing) { + LaunchedEffect(true) { + // fetch something + delay(1500) + libraryViewModel.refresh() + pullToRefreshState.endRefresh() + } + } val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState() Box( modifier = Modifier + .padding(top = paddingValues.calculateTopPadding()) .fillMaxSize() - .pullRefresh(pullRefreshState) + .nestedScroll(pullToRefreshState.nestedScrollConnection) ) { + Column { + LibraryFilterBar() + HorizontalDivider() + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items( + items = (uiState as LibraryUiState.Success).items, + key = { item -> item.savedItem.savedItemId } + ) { cardDataWithLabels -> + val swipeThreshold = 0.45f - LazyColumn( - state = listState, - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - ) { - item { - LibraryFilterBar(libraryViewModel) - } - items( - items = (uiState as LibraryUiState.Success).items, - key = { item -> item.savedItem.savedItemId } - ) { cardDataWithLabels -> - val swipeThreshold = 0.45f - - val currentThresholdFraction = remember { mutableStateOf(0f) } - val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem) - val swipeState = rememberDismissState( - confirmStateChange = { - when(it) { - DismissValue.Default -> { - return@rememberDismissState false - } - DismissValue.DismissedToEnd -> { - if (currentThresholdFraction.value < swipeThreshold) { + val currentThresholdFraction = remember { mutableStateOf(0f) } + val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem) + val swipeState = rememberDismissState( + confirmStateChange = { + when(it) { + DismissValue.Default -> { return@rememberDismissState false } - } - DismissValue.DismissedToStart -> { - if (currentThresholdFraction.value < swipeThreshold) { - return@rememberDismissState false + DismissValue.DismissedToEnd -> { + if (currentThresholdFraction.value < swipeThreshold) { + return@rememberDismissState false + } + } + DismissValue.DismissedToStart -> { + if (currentThresholdFraction.value < swipeThreshold) { + return@rememberDismissState false + } } } - } - if (it == DismissValue.DismissedToEnd) { // Archiving/UnArchiving. - if (currentItem.isArchived) { - libraryViewModel.unarchiveSavedItem(currentItem.savedItemId) - } else { - libraryViewModel.archiveSavedItem(currentItem.savedItemId) + if (it == DismissValue.DismissedToEnd) { // Archiving/UnArchiving. + if (currentItem.isArchived) { + libraryViewModel.unarchiveSavedItem(currentItem.savedItemId) + } else { + libraryViewModel.archiveSavedItem(currentItem.savedItemId) + } + } else if (it == DismissValue.DismissedToStart) { // Deleting. + libraryViewModel.deleteSavedItem(currentItem.savedItemId) } - } else if (it == DismissValue.DismissedToStart) { // Deleting. - libraryViewModel.deleteSavedItem(currentItem.savedItemId) - } - true - } - ) - SwipeToDismiss( - state = swipeState, - directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart), - dismissThresholds = { FractionalThreshold(swipeThreshold) }, - background = { - val direction = swipeState.dismissDirection ?: return@SwipeToDismiss - val color by animateColorAsState( - when (swipeState.targetValue) { - DismissValue.Default -> Color.LightGray - DismissValue.DismissedToEnd -> Color.Green - DismissValue.DismissedToStart -> Color.Red - }, label = "backgroundColor" - ) - val alignment = when (direction) { - DismissDirection.StartToEnd -> Alignment.CenterStart - DismissDirection.EndToStart -> Alignment.CenterEnd + true } - val icon = when (direction) { - DismissDirection.StartToEnd -> if (currentItem.isArchived) Icons.Default.Unarchive else Icons.Default.Archive - DismissDirection.EndToStart -> Icons.Default.Delete - } - val scale by animateFloatAsState( - if (swipeState.targetValue == DismissValue.Default) 0.75f else 1f, - label = "scaleAnimation" - ) - - Box( - Modifier - .fillMaxSize() - .background(color) - .padding(horizontal = 20.dp), - contentAlignment = alignment - ) { - currentThresholdFraction.value = swipeState.progress.fraction - Icon( - icon, - contentDescription = null, - modifier = Modifier.scale(scale) + ) + SwipeToDismiss( + state = swipeState, + directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart), + dismissThresholds = { FractionalThreshold(swipeThreshold) }, + background = { + val direction = swipeState.dismissDirection ?: return@SwipeToDismiss + val color by animateColorAsState( + when (swipeState.targetValue) { + DismissValue.Default -> Color.LightGray + DismissValue.DismissedToEnd -> Color.Green + DismissValue.DismissedToStart -> Color.Red + }, label = "backgroundColor" ) - } - }, - dismissContent = { - val selected = - currentItem.savedItemId == selectedItem?.savedItem?.savedItemId - val savedItem = SavedItemWithLabelsAndHighlights( - savedItem = cardDataWithLabels.savedItem, - labels = cardDataWithLabels.labels, - highlights = cardDataWithLabels.highlights - ) - SavedItemCard( - selected = selected, - savedItemViewModel = libraryViewModel, - savedItem = savedItem, - onClickHandler = { - libraryViewModel.actionsMenuItemLiveData.postValue(null) - val activityClass = - if (currentItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java - val intent = Intent(context, activityClass) - intent.putExtra("SAVED_ITEM_SLUG", currentItem.slug) - context.startActivity(intent) - }, - actionHandler = { - libraryViewModel.handleSavedItemAction( - currentItem.savedItemId, - it + val alignment = when (direction) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd + } + val icon = when (direction) { + DismissDirection.StartToEnd -> if (currentItem.isArchived) Icons.Default.Unarchive else Icons.Default.Archive + DismissDirection.EndToStart -> Icons.Default.Delete + } + val scale by animateFloatAsState( + if (swipeState.targetValue == DismissValue.Default) 0.75f else 1f, + label = "scaleAnimation" + ) + + Box( + Modifier + .fillMaxSize() + .background(color) + .padding(horizontal = 20.dp), + contentAlignment = alignment + ) { + currentThresholdFraction.value = swipeState.progress.fraction + Icon( + icon, + contentDescription = null, + modifier = Modifier.scale(scale) ) } - ) - }, - ) - when { - swipeState.isDismissed(DismissDirection.EndToStart) -> Reset(state = swipeState) - swipeState.isDismissed(DismissDirection.StartToEnd) -> Reset(state = swipeState) + }, + dismissContent = { + val selected = + currentItem.savedItemId == selectedItem?.savedItem?.savedItemId + val savedItem = SavedItemWithLabelsAndHighlights( + savedItem = cardDataWithLabels.savedItem, + labels = cardDataWithLabels.labels, + highlights = cardDataWithLabels.highlights + ) + SavedItemCard( + selected = selected, + savedItemViewModel = libraryViewModel, + savedItem = savedItem, + onClickHandler = { + libraryViewModel.actionsMenuItemLiveData.postValue(null) + val activityClass = + if (currentItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + val intent = Intent(context, activityClass) + intent.putExtra("SAVED_ITEM_SLUG", currentItem.slug) + context.startActivity(intent) + }, + actionHandler = { + libraryViewModel.handleSavedItemAction( + currentItem.savedItemId, + it + ) + } + ) + }, + ) + when { + swipeState.isDismissed(DismissDirection.EndToStart) -> Reset(state = swipeState) + swipeState.isDismissed(DismissDirection.StartToEnd) -> Reset(state = swipeState) + } } } } @@ -453,10 +459,9 @@ fun LibraryViewContent( } } - PullRefreshIndicator( - refreshing = libraryViewModel.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter) + PullToRefreshContainer( + modifier = Modifier.align(Alignment.TopCenter), + state = pullToRefreshState, ) // LabelsSelectionSheet(viewModel = libraryViewModel) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt index cd138c6ab..f231db99e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt @@ -32,16 +32,22 @@ import com.apollographql.apollo3.api.Optional import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.time.Instant import javax.inject.Inject +@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class LibraryViewModel @Inject constructor( private val networker: Networker, @@ -67,8 +73,7 @@ class LibraryViewModel @Inject constructor( ) ) - // Correct way - but not working -/* val uiState: StateFlow = _libraryQuery.flatMapLatest { query -> + val uiState: StateFlow = _libraryQuery.flatMapLatest { query -> libraryRepository.getSavedItems(query) } .map(LibraryUiState::Success) @@ -76,24 +81,8 @@ class LibraryViewModel @Inject constructor( scope = viewModelScope, started = SharingStarted.Lazily, initialValue = LibraryUiState.Loading - )*/ + ) - // This approach needs to be replaced with the StateFlow above after fixing Room Flow - private val _uiState = MutableStateFlow(LibraryUiState.Loading) - val uiState: StateFlow = _uiState - - init { - loadSavedItems() - } - - private fun loadSavedItems() { - viewModelScope.launch { - libraryRepository.getSavedItems(_libraryQuery.value) - .collect { favoriteNews -> - _uiState.value = LibraryUiState.Success(favoriteNews) - } - } - } val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) @@ -265,7 +254,6 @@ class LibraryViewModel @Inject constructor( excludedLabels = excludeLabels, allowedContentReaders = allowedContentReaders ) - loadSavedItems() } } @@ -345,17 +333,13 @@ class LibraryViewModel @Inject constructor( SavedItemAction.MarkRead -> { viewModelScope.launch { - _uiState.value = LibraryUiState.Success(emptyList()) libraryRepository.updateReadingProgress(itemID, 100.0, 0) - loadSavedItems() } } SavedItemAction.MarkUnread -> { viewModelScope.launch { - _uiState.value = LibraryUiState.Success(emptyList()) libraryRepository.updateReadingProgress(itemID, 0.0, 0) - loadSavedItems() } } }