From b7fd8a63268b55570c6819f1adb708e0a81ad12d Mon Sep 17 00:00:00 2001 From: Gannon Date: Thu, 4 Jan 2024 22:11:43 -0500 Subject: [PATCH] Update LibraryView to use ModalBottomSheet using the modalBottomSheet will support back action, tapping off the bottomsheet, and swiping down --- .../app/omnivore/omnivore/EventTracker.kt | 11 +- .../omnivore/ui/auth/LoginViewModel.kt | 2 +- .../ui/library/LibraryBottomSheetState.kt | 8 + .../omnivore/ui/library/LibraryFilterBar.kt | 2 +- .../omnivore/ui/library/LibraryView.kt | 731 +++++++++--------- .../omnivore/ui/library/LibraryViewModel.kt | 643 +++++++-------- 6 files changed, 731 insertions(+), 666 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryBottomSheetState.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/EventTracker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/EventTracker.kt index e5ce9fbcc..38abeeda0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/EventTracker.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/EventTracker.kt @@ -24,12 +24,15 @@ class EventTracker @Inject constructor(val app: Context) { PostHog.setSingletonInstance(posthog) } - fun registerUser(userID: String, intercomHash: String?) { + fun registerUser(userID: String, intercomHash: String?, isDebug: Boolean) { posthog.identify(userID) - Intercom.client().loginIdentifiedUser(Registration.create().withUserId(userID)) - intercomHash?.let { intercomHash -> - Intercom.client().setUserHash(intercomHash) + if (!isDebug) { + Intercom.client().loginIdentifiedUser(Registration.create().withUserId(userID)) + intercomHash?.let { intercomHash -> + Intercom.client().setUserHash(intercomHash) + } } + } fun track(eventName: String, properties: Properties = Properties()) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt index 462e606e7..ff9b40868 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt @@ -130,7 +130,7 @@ class LoginViewModel @Inject constructor( viewModelScope.launch { val viewer = networker.viewer() viewer?.let { - eventTracker.registerUser(viewer.userID, viewer.intercomHash) + eventTracker.registerUser(viewer.userID, viewer.intercomHash, BuildConfig.DEBUG) } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryBottomSheetState.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryBottomSheetState.kt new file mode 100644 index 000000000..4bd866b45 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryBottomSheetState.kt @@ -0,0 +1,8 @@ +package app.omnivore.omnivore.ui.library + +enum class LibraryBottomSheetState { + HIDDEN, + ADD_LINK, + LABEL, + EDIT +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt index 06099ea77..576b21e9d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryFilterBar.kt @@ -63,7 +63,7 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) { modifier = Modifier.padding(end = 6.dp) ) AssistChip( - onClick = { viewModel.showLabelsSelectionSheetLiveData.value = true }, + onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL }, label = { Text(stringResource(R.string.library_filter_bar_label_labels)) }, trailingIcon = { Icon( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt index 6086a1e8e..76850d9af 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryView.kt @@ -3,37 +3,50 @@ package app.omnivore.omnivore.ui.library import android.content.Intent import android.util.Log import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.FloatTweenSpec import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* import androidx.compose.material.DismissDirection import androidx.compose.material.DismissState -import androidx.compose.material.FractionalThreshold import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FractionalThreshold +import androidx.compose.material.Icon +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.material.SwipeToDismiss +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.SwipeToDismiss -import androidx.compose.material.Icon -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Archive -import androidx.compose.material.icons.filled.Unarchive -import androidx.compose.material3.* +import androidx.compose.material.rememberDismissState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -47,370 +60,386 @@ import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent import app.omnivore.omnivore.ui.components.LabelsViewModel import app.omnivore.omnivore.ui.editinfo.EditInfoSheetContent import app.omnivore.omnivore.ui.editinfo.EditInfoViewModel -import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity import app.omnivore.omnivore.ui.save.SaveState import app.omnivore.omnivore.ui.save.SaveViewModel +import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable fun LibraryView( - libraryViewModel: LibraryViewModel, - labelsViewModel: LabelsViewModel, - saveViewModel: SaveViewModel, - editInfoViewModel: EditInfoViewModel, - navController: NavHostController + libraryViewModel: LibraryViewModel, + labelsViewModel: LabelsViewModel, + saveViewModel: SaveViewModel, + editInfoViewModel: EditInfoViewModel, + navController: NavHostController ) { - val scaffoldState: ScaffoldState = rememberScaffoldState() - val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - val showAddLinkSheet: Boolean by libraryViewModel.showAddLinkSheetLiveData.observeAsState(false) - val showEditInfoSheet: Boolean by libraryViewModel.showEditInfoSheetLiveData.observeAsState(false) + val scaffoldState: ScaffoldState = rememberScaffoldState() - val coroutineScope = rememberCoroutineScope() - val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden, - confirmValueChange = { it != ModalBottomSheetValue.Hidden } - ) + val coroutineScope = rememberCoroutineScope() - if (showLabelsSelectionSheet || showAddLinkSheet || showEditInfoSheet) { - coroutineScope.launch { - modalBottomSheetState.show() - } - } else { - coroutineScope.launch { - modalBottomSheetState.hide() - } - } - - libraryViewModel.snackbarMessage?.let { - coroutineScope.launch { - scaffoldState.snackbarHostState.showSnackbar(it) - libraryViewModel.clearSnackbarMessage() - } - } - - ModalBottomSheetLayout( - sheetBackgroundColor = Color.Transparent, - sheetState = modalBottomSheetState, - sheetContent = { - BottomSheetContent(libraryViewModel, labelsViewModel, saveViewModel,editInfoViewModel) - Spacer(modifier = Modifier.weight(1.0F)) - } - ) { - Scaffold( - scaffoldState = scaffoldState, - topBar = { - LibraryNavigationBar( - savedItemViewModel = libraryViewModel, - onSearchClicked = { navController.navigate(Routes.Search.route) }, - onAddLinkClicked = { libraryViewModel.showAddLinkSheetLiveData.value = true }, - onSettingsIconClick = { navController.navigate(Routes.Settings.route) } - ) - }, - ) { paddingValues -> - LibraryViewContent( - libraryViewModel, - modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()) - ) - } - } -} - -@Composable -fun BottomSheetContent(libraryViewModel: LibraryViewModel, - labelsViewModel: LabelsViewModel, - saveViewModel: SaveViewModel, - editInfoViewModel: EditInfoViewModel -) { - val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) - val showAddLinkSheet: Boolean by libraryViewModel.showAddLinkSheetLiveData.observeAsState(false) - val showEditInfoSheet: Boolean by libraryViewModel.showEditInfoSheetLiveData.observeAsState(false) - val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit() - val labels: List by libraryViewModel.savedItemLabelsLiveData.observeAsState(listOf()) - - if (showLabelsSelectionSheet) { - BottomSheetUI { - if (currentSavedItemData != null) { - LabelsSelectionSheetContent( - labels = labels, - labelsViewModel = labelsViewModel, - initialSelectedLabels = currentSavedItemData.labels, - onCancel = { - libraryViewModel.showLabelsSelectionSheetLiveData.value = false - libraryViewModel.currentItemLiveData.value = null - }, - isLibraryMode = false, - onSave = { - if (it != labels) { - libraryViewModel.updateSavedItemLabels( - savedItemID = currentSavedItemData.savedItem.savedItemId, - labels = it - ) - } - libraryViewModel.currentItemLiveData.value = null - libraryViewModel.showLabelsSelectionSheetLiveData.value = false - }, - onCreateLabel = { newLabelName, labelHexValue -> - libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue) - } - ) - } else { // Is used in library mode - LabelsSelectionSheetContent( - labels = labels, - labelsViewModel = labelsViewModel, - initialSelectedLabels = libraryViewModel.activeLabelsLiveData.value ?: listOf(), - onCancel = { libraryViewModel.showLabelsSelectionSheetLiveData.value = false }, - isLibraryMode = true, - onSave = { - libraryViewModel.updateAppliedLabels(it) - libraryViewModel.currentItemLiveData.value = null - libraryViewModel.showLabelsSelectionSheetLiveData.value = false - }, - onCreateLabel = { newLabelName, labelHexValue -> - libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue) - } - ) - } - } - } else if (showAddLinkSheet) { - BottomSheetUI { - AddLinkSheetContent( - viewModel = saveViewModel, - onCancel = { - libraryViewModel.showAddLinkSheetLiveData.value = false - saveViewModel.state.value = SaveState.DEFAULT - }, - onLinkAdded = { - libraryViewModel.showAddLinkSheetLiveData.value = false - saveViewModel.state.value = SaveState.DEFAULT - } - ) - } - } else if (showEditInfoSheet) { - BottomSheetUI { - EditInfoSheetContent( - savedItemId = currentSavedItemData?.savedItem?.savedItemId, - title = currentSavedItemData?.savedItem?.title, - author = currentSavedItemData?.savedItem?.author, - description = currentSavedItemData?.savedItem?.descriptionText, - viewModel = editInfoViewModel, - onCancel = { - libraryViewModel.showEditInfoSheetLiveData.value = false - libraryViewModel.currentItemLiveData.value = null - }, - onUpdated = { - libraryViewModel.showEditInfoSheetLiveData.value = false - libraryViewModel.currentItemLiveData.value = null - libraryViewModel.refresh() - } - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) -@Composable -fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { - val context = LocalContext.current - val listState = rememberLazyListState() - - val pullRefreshState = rememberPullRefreshState( - refreshing = libraryViewModel.isRefreshing, - onRefresh = { libraryViewModel.refresh() } - ) - - val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState() - val cardsData: List by libraryViewModel.itemsLiveData.observeAsState( - listOf() - ) - - Box( - modifier = Modifier - .fillMaxSize() - .pullRefresh(pullRefreshState) - ) { - - LazyColumn( - state = listState, - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - .padding(horizontal = 6.dp) - ) { - item { - LibraryFilterBar(libraryViewModel) - } - items( - items = cardsData, - key = { item -> item.savedItem.savedItemId } - ) { cardDataWithLabels -> - val swipeThreshold = 0.40f - - val currentThresholdFraction = remember { mutableStateOf(0f) } - val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem) - val swipeState = rememberDismissState( - confirmStateChange = { - if (it == DismissValue.DismissedToEnd || - currentThresholdFraction.value < swipeThreshold || - currentThresholdFraction.value > 1.0f) { - false - } - - 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) - } - - true - } - ) - SwipeToDismiss( - state = swipeState, - modifier = Modifier.padding(vertical = 4.dp), - 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 - } - 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) - ) - } - }, - dismissContent = { - val selected = currentItem.savedItemId == selectedItem?.savedItem?.savedItemId - SavedItemCard( - selected = selected, - savedItemViewModel = libraryViewModel, - savedItem = cardDataWithLabels, - 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) - } - } - } - - InfiniteListHandler(listState = listState) { - if (cardsData.isEmpty()) { - Log.d("sync", "loading with load func") - libraryViewModel.initialLoad() - } else { - Log.d("sync", "loading with search api") - libraryViewModel.loadUsingSearchAPI() - } - } - - PullRefreshIndicator( - refreshing = libraryViewModel.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter) + val showBottomSheet: LibraryBottomSheetState by libraryViewModel.bottomSheetState.observeAsState( + LibraryBottomSheetState.HIDDEN ) - // LabelsSelectionSheet(viewModel = libraryViewModel) - } + libraryViewModel.snackbarMessage?.let { + coroutineScope.launch { + scaffoldState.snackbarHostState.showSnackbar(it) + libraryViewModel.clearSnackbarMessage() + } + } + + when (showBottomSheet) { + LibraryBottomSheetState.ADD_LINK -> { + AddLinkBottomSheet(saveViewModel) { + libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN + } + } + + LibraryBottomSheetState.LABEL -> { + LabelBottomSheet( + libraryViewModel, + labelsViewModel + ) { + libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN + } + } + + LibraryBottomSheetState.EDIT -> { + EditBottomSheet( + editInfoViewModel, + libraryViewModel + ) { + libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN + } + } + + LibraryBottomSheetState.HIDDEN -> { + } + } + + Scaffold( + scaffoldState = scaffoldState, + topBar = { + LibraryNavigationBar( + savedItemViewModel = libraryViewModel, + onSearchClicked = { navController.navigate(Routes.Search.route) }, + onAddLinkClicked = { showAddLinkBottomSheet(libraryViewModel) }, + onSettingsIconClick = { navController.navigate(Routes.Settings.route) } + ) + }, + ) { paddingValues -> + LibraryViewContent( + libraryViewModel, + modifier = Modifier + .padding(top = paddingValues.calculateTopPadding()) + ) + } +} + +fun showAddLinkBottomSheet(libraryViewModel: LibraryViewModel) { + libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.ADD_LINK +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LabelBottomSheet( + libraryViewModel: LibraryViewModel, + labelsViewModel: LabelsViewModel, + onDismiss: () -> Unit = {} +) { + ModalBottomSheet(onDismissRequest = { onDismiss() }) { + val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit() + val labels: List by libraryViewModel.savedItemLabelsLiveData.observeAsState( + listOf() + ) + if (currentSavedItemData != null) { + LabelsSelectionSheetContent( + labels = labels, + labelsViewModel = labelsViewModel, + initialSelectedLabels = currentSavedItemData.labels, + onCancel = { + libraryViewModel.currentItemLiveData.value = null + onDismiss() + }, + isLibraryMode = false, + onSave = { + if (it != labels) { + libraryViewModel.updateSavedItemLabels( + savedItemID = currentSavedItemData.savedItem.savedItemId, + labels = it + ) + } + libraryViewModel.currentItemLiveData.value = null + onDismiss() + }, + onCreateLabel = { newLabelName, labelHexValue -> + libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue) + } + ) + } else { // Is used in library mode + LabelsSelectionSheetContent( + labels = labels, + labelsViewModel = labelsViewModel, + initialSelectedLabels = libraryViewModel.activeLabelsLiveData.value ?: listOf(), + onCancel = { onDismiss() }, + isLibraryMode = true, + onSave = { + libraryViewModel.updateAppliedLabels(it) + libraryViewModel.currentItemLiveData.value = null + onDismiss() + }, + onCreateLabel = { newLabelName, labelHexValue -> + libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue) + } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddLinkBottomSheet( + saveViewModel: SaveViewModel, + onDismiss: () -> Unit = {} +) { + ModalBottomSheet(onDismissRequest = { onDismiss() }) { + AddLinkSheetContent( + viewModel = saveViewModel, + onCancel = { + saveViewModel.state.value = SaveState.DEFAULT + onDismiss() + }, + onLinkAdded = { + saveViewModel.state.value = SaveState.DEFAULT + onDismiss() + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditBottomSheet( + editInfoViewModel: EditInfoViewModel, + libraryViewModel: LibraryViewModel, + onDismiss: () -> Unit = {} +) { + ModalBottomSheet(onDismissRequest = { onDismiss() }) { + val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit() + EditInfoSheetContent( + savedItemId = currentSavedItemData?.savedItem?.savedItemId, + title = currentSavedItemData?.savedItem?.title, + author = currentSavedItemData?.savedItem?.author, + description = currentSavedItemData?.savedItem?.descriptionText, + viewModel = editInfoViewModel, + onCancel = { + libraryViewModel.currentItemLiveData.value = null + onDismiss() + }, + onUpdated = { + libraryViewModel.currentItemLiveData.value = null + libraryViewModel.refresh() + onDismiss() + } + ) + } +} + + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { + val context = LocalContext.current + val listState = rememberLazyListState() + + val pullRefreshState = rememberPullRefreshState( + refreshing = libraryViewModel.isRefreshing, + onRefresh = { libraryViewModel.refresh() } + ) + + val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState() + val cardsData: List by libraryViewModel.itemsLiveData.observeAsState( + listOf() + ) + + Box( + modifier = Modifier + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + .padding(horizontal = 6.dp) + ) { + item { + LibraryFilterBar(libraryViewModel) + } + items( + items = cardsData, + key = { item -> item.savedItem.savedItemId } + ) { cardDataWithLabels -> + val swipeThreshold = 0.40f + + val currentThresholdFraction = remember { mutableStateOf(0f) } + val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem) + val swipeState = rememberDismissState( + confirmStateChange = { + if (it == DismissValue.DismissedToEnd || + currentThresholdFraction.value < swipeThreshold || + currentThresholdFraction.value > 1.0f + ) { + false + } + + 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) + } + + true + } + ) + SwipeToDismiss( + state = swipeState, + modifier = Modifier.padding(vertical = 4.dp), + 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 + } + 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) + ) + } + }, + dismissContent = { + val selected = + currentItem.savedItemId == selectedItem?.savedItem?.savedItemId + SavedItemCard( + selected = selected, + savedItemViewModel = libraryViewModel, + savedItem = cardDataWithLabels, + 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) + } + } + } + + InfiniteListHandler(listState = listState) { + if (cardsData.isEmpty()) { + Log.d("sync", "loading with load func") + libraryViewModel.initialLoad() + } else { + Log.d("sync", "loading with search api") + libraryViewModel.loadUsingSearchAPI() + } + } + + PullRefreshIndicator( + refreshing = libraryViewModel.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + + // LabelsSelectionSheet(viewModel = libraryViewModel) + } } @OptIn(ExperimentalMaterialApi::class) @Composable private fun Reset(state: DismissState) { - val scope = rememberCoroutineScope() - LaunchedEffect(key1 = state.dismissDirection) { - scope.launch { - state.reset() - state.animateTo(DismissValue.Default, FloatTweenSpec(duration= 0, delay = 0)) + val scope = rememberCoroutineScope() + LaunchedEffect(key1 = state.dismissDirection) { + scope.launch { + state.reset() + state.animateTo(DismissValue.Default, FloatTweenSpec(duration = 0, delay = 0)) + } } - } } -@Composable -private fun BottomSheetUI(content: @Composable () -> Unit) { - Box( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)) - .background(Color.White) - .statusBarsPadding() - ) { - content() - } -} - - @Composable fun InfiniteListHandler( - listState: LazyListState, - buffer: Int = 2, - onLoadMore: () -> Unit + listState: LazyListState, + buffer: Int = 2, + onLoadMore: () -> Unit ) { - val loadMore = remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + val loadMore = remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - lastVisibleItemIndex > (totalItemsNumber - buffer) + lastVisibleItemIndex > (totalItemsNumber - buffer) + } } - } - LaunchedEffect(loadMore) { - snapshotFlow { loadMore.value } - .distinctUntilChanged() - .collect { - onLoadMore() - } - } + LaunchedEffect(loadMore) { + snapshotFlow { loadMore.value } + .distinctUntilChanged() + .collect { + onLoadMore() + } + } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt index a7983cdfb..02c4742f8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryViewModel.kt @@ -20,155 +20,160 @@ import javax.inject.Inject @HiltViewModel class LibraryViewModel @Inject constructor( - private val networker: Networker, - private val dataService: DataService, - private val datastoreRepo: DatastoreRepository, - private val resourceProvider: ResourceProvider -): ViewModel(), SavedItemViewModel { - private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) + private val networker: Networker, + private val dataService: DataService, + private val datastoreRepo: DatastoreRepository, + private val resourceProvider: ResourceProvider +) : ViewModel(), SavedItemViewModel { + private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) - private var cursor: String? = null - private var librarySearchCursor: String? = null + private var cursor: String? = null + private var librarySearchCursor: String? = null - // These are used to make sure we handle search result - // responses in the right order - private var searchIdx = 0 - private var receivedIdx = 0 + // These are used to make sure we handle search result + // responses in the right order + private var searchIdx = 0 + private var receivedIdx = 0 - var snackbarMessage by mutableStateOf(null) - private set + var snackbarMessage by mutableStateOf(null) + private set - // Live Data - private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData( - allowedArchiveStates = listOf(0), - sortKey = "newest", - requiredLabels = listOf(), - excludedLabels = listOf(), - allowedContentReaders = listOf("WEB", "PDF", "EPUB") - ) - val itemsLiveData = MediatorLiveData>() - val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) - val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) - val showLabelsSelectionSheetLiveData = MutableLiveData(false) - val showEditInfoSheetLiveData = MutableLiveData(false) - val showAddLinkSheetLiveData = MutableLiveData(false) - val currentItemLiveData = MutableLiveData(null) - val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() - val activeLabelsLiveData = MutableLiveData>(listOf()) + // Live Data + private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData( + allowedArchiveStates = listOf(0), + sortKey = "newest", + requiredLabels = listOf(), + excludedLabels = listOf(), + allowedContentReaders = listOf("WEB", "PDF", "EPUB") + ) + val itemsLiveData = MediatorLiveData>() + val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) + val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) + val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN) + val currentItemLiveData = MutableLiveData(null) + val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() + val activeLabelsLiveData = MutableLiveData>(listOf()) - override val actionsMenuItemLiveData = MutableLiveData(null) + override val actionsMenuItemLiveData = MutableLiveData(null) - var isRefreshing by mutableStateOf(false) - private var hasLoadedInitialFilters = false + var isRefreshing by mutableStateOf(false) + private var hasLoadedInitialFilters = false - private fun loadInitialFilterValues() { - if (hasLoadedInitialFilters) { return } - hasLoadedInitialFilters = false - - viewModelScope.launch { - withContext(Dispatchers.IO) { - dataService.syncLabels() - } - } - - viewModelScope.launch { - handleFilterChanges() - for (slug in contentRequestChannel) { - CoroutineScope(Dispatchers.IO).launch { - dataService.fetchSavedItemContent(slug) + private fun loadInitialFilterValues() { + if (hasLoadedInitialFilters) { + return } - } - } - } + hasLoadedInitialFilters = false - fun clearSnackbarMessage() { - snackbarMessage = null - } - - fun refresh() { - cursor = null - librarySearchCursor = null - isRefreshing = true - load() - } - - private fun getLastSyncTime(): Instant? = runBlocking { - datastoreRepo.getString(DatastoreKeys.libraryLastSyncTimestamp)?.let { - try { - return@let Instant.parse(it) - } catch (e: Exception) { - return@let null - } - } - } - - fun initialLoad() { - if (getLastSyncTime() == null) { - hasLoadedInitialFilters = false - cursor = null - librarySearchCursor = null - searchIdx = 0 - receivedIdx = 0 - } - - if (hasLoadedInitialFilters) { return } - load() - } - - fun load() { - loadInitialFilterValues() - - viewModelScope.launch { - syncItems() - loadUsingSearchAPI() - } - } - - fun loadUsingSearchAPI() { - viewModelScope.launch { - withContext(Dispatchers.IO) { - val result = dataService.librarySearch(cursor = librarySearchCursor, query = searchQueryString()) - result.cursor?.let { - librarySearchCursor = it - } - CoroutineScope(Dispatchers.Main).launch { - isRefreshing = false + viewModelScope.launch { + withContext(Dispatchers.IO) { + dataService.syncLabels() + } } - result.savedItems.map { - val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug) - - if (!isSavedInDB) { - delay(2000) - contentRequestChannel.send(it.savedItem.slug) - } + viewModelScope.launch { + handleFilterChanges() + for (slug in contentRequestChannel) { + CoroutineScope(Dispatchers.IO).launch { + dataService.fetchSavedItemContent(slug) + } + } } - } } - } - fun updateSavedItemFilter(filter: SavedItemFilter) { - viewModelScope.launch { - datastoreRepo.putString(DatastoreKeys.lastUsedSavedItemFilter, filter.rawValue) - appliedFilterLiveData.value = filter - handleFilterChanges() + fun clearSnackbarMessage() { + snackbarMessage = null } - } - fun updateSavedItemSortFilter(filter: SavedItemSortFilter) { - viewModelScope.launch { - datastoreRepo.putString(DatastoreKeys.lastUsedSavedItemSortFilter, filter.rawValue) - appliedSortFilterLiveData.value = filter - handleFilterChanges() + fun refresh() { + cursor = null + librarySearchCursor = null + isRefreshing = true + load() } - } - fun updateAppliedLabels(labels: List) { - viewModelScope.launch { - activeLabelsLiveData.value = labels - handleFilterChanges() + private fun getLastSyncTime(): Instant? = runBlocking { + datastoreRepo.getString(DatastoreKeys.libraryLastSyncTimestamp)?.let { + try { + return@let Instant.parse(it) + } catch (e: Exception) { + return@let null + } + } + } + + fun initialLoad() { + if (getLastSyncTime() == null) { + hasLoadedInitialFilters = false + cursor = null + librarySearchCursor = null + searchIdx = 0 + receivedIdx = 0 + } + + if (hasLoadedInitialFilters) { + return + } + load() + } + + fun load() { + loadInitialFilterValues() + + viewModelScope.launch { + syncItems() + loadUsingSearchAPI() + } + } + + fun loadUsingSearchAPI() { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val result = dataService.librarySearch( + cursor = librarySearchCursor, + query = searchQueryString() + ) + result.cursor?.let { + librarySearchCursor = it + } + CoroutineScope(Dispatchers.Main).launch { + isRefreshing = false + } + + result.savedItems.map { + val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug) + + if (!isSavedInDB) { + delay(2000) + contentRequestChannel.send(it.savedItem.slug) + } + } + } + } + } + + fun updateSavedItemFilter(filter: SavedItemFilter) { + viewModelScope.launch { + datastoreRepo.putString(DatastoreKeys.lastUsedSavedItemFilter, filter.rawValue) + appliedFilterLiveData.value = filter + handleFilterChanges() + } + } + + fun updateSavedItemSortFilter(filter: SavedItemSortFilter) { + viewModelScope.launch { + datastoreRepo.putString(DatastoreKeys.lastUsedSavedItemSortFilter, filter.rawValue) + appliedSortFilterLiveData.value = filter + handleFilterChanges() + } + } + + fun updateAppliedLabels(labels: List) { + viewModelScope.launch { + activeLabelsLiveData.value = labels + handleFilterChanges() + } } - } // fun sortKey(appliedSortKey: String) { // when(appliedSortKey) { @@ -176,208 +181,228 @@ class LibraryViewModel @Inject constructor( // } // } - private fun handleFilterChanges() { - librarySearchCursor = null + private fun handleFilterChanges() { + librarySearchCursor = null - if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { - val sortKey = when (appliedSortFilterLiveData.value) { - SavedItemSortFilter.NEWEST -> "newest" - SavedItemSortFilter.OLDEST -> "oldest" - SavedItemSortFilter.RECENTLY_READ -> "recentlyRead" - SavedItemSortFilter.RECENTLY_PUBLISHED -> "recentlyPublished" - else -> "newest" - } + if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { + val sortKey = when (appliedSortFilterLiveData.value) { + SavedItemSortFilter.NEWEST -> "newest" + SavedItemSortFilter.OLDEST -> "oldest" + SavedItemSortFilter.RECENTLY_READ -> "recentlyRead" + SavedItemSortFilter.RECENTLY_PUBLISHED -> "recentlyPublished" + else -> "newest" + } - val allowedArchiveStates = when (appliedFilterLiveData.value) { - SavedItemFilter.ALL -> listOf(0, 1) - SavedItemFilter.ARCHIVED -> listOf(1) - else -> listOf(0) - } + val allowedArchiveStates = when (appliedFilterLiveData.value) { + SavedItemFilter.ALL -> listOf(0, 1) + SavedItemFilter.ARCHIVED -> listOf(1) + else -> listOf(0) + } - val allowedContentReaders = when(appliedFilterLiveData.value) { - SavedItemFilter.FILES -> listOf("PDF", "EPUB") - else -> listOf("WEB", "PDF", "EPUB") - } + val allowedContentReaders = when (appliedFilterLiveData.value) { + SavedItemFilter.FILES -> listOf("PDF", "EPUB") + else -> listOf("WEB", "PDF", "EPUB") + } - var requiredLabels = when(appliedFilterLiveData.value) { - SavedItemFilter.NEWSLETTERS -> listOf("Newsletter") - SavedItemFilter.FEEDS -> listOf("RSS") - else -> (activeLabelsLiveData.value ?: listOf()).map { it.name } - } + var requiredLabels = when (appliedFilterLiveData.value) { + SavedItemFilter.NEWSLETTERS -> listOf("Newsletter") + SavedItemFilter.FEEDS -> listOf("RSS") + else -> (activeLabelsLiveData.value ?: listOf()).map { it.name } + } - activeLabelsLiveData.value?.let { it -> - requiredLabels = requiredLabels + it.map { it.name } - } + activeLabelsLiveData.value?.let { it -> + requiredLabels = requiredLabels + it.map { it.name } + } - val excludeLabels = when(appliedFilterLiveData.value) { - SavedItemFilter.READ_LATER -> listOf("Newsletter", "RSS") - else -> listOf() - } + val excludeLabels = when (appliedFilterLiveData.value) { + SavedItemFilter.READ_LATER -> listOf("Newsletter", "RSS") + else -> listOf() + } - val newData = dataService.db.savedItemDao().filteredLibraryData( - allowedArchiveStates = allowedArchiveStates, - sortKey = sortKey, - requiredLabels = requiredLabels, - excludedLabels = excludeLabels, - allowedContentReaders = allowedContentReaders - ) + val newData = dataService.db.savedItemDao().filteredLibraryData( + allowedArchiveStates = allowedArchiveStates, + sortKey = sortKey, + requiredLabels = requiredLabels, + excludedLabels = excludeLabels, + allowedContentReaders = allowedContentReaders + ) - itemsLiveData.removeSource(itemsLiveDataInternal) - itemsLiveDataInternal = newData - itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue) - } - } - - private suspend fun syncItems() { - val syncStart = Instant.now() - val lastSyncDate = getLastSyncTime() ?: Instant.MIN - - withContext(Dispatchers.IO) { - performItemSync(cursor = null, since = lastSyncDate.toString(), count = 0, startTime = syncStart.toString()) - CoroutineScope(Dispatchers.Main).launch { - isRefreshing = false - } - } - } - - private suspend fun performItemSync(cursor: String?, since: String, count: Int, startTime: String, isInitialBatch: Boolean = true) { - dataService.syncOfflineItemsWithServerIfNeeded() - val result = dataService.sync(since = since, cursor = cursor, limit = 20) - - // Fetch content for the initial batch only - if (isInitialBatch) { - for (slug in result.savedItemSlugs) { - delay(250) - contentRequestChannel.send(slug) - } + itemsLiveData.removeSource(itemsLiveDataInternal) + itemsLiveDataInternal = newData + itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue) + } } - val totalCount = count + result.count + private suspend fun syncItems() { + val syncStart = Instant.now() + val lastSyncDate = getLastSyncTime() ?: Instant.MIN - if (!result.hasError && result.hasMoreItems && result.cursor != null) { - performItemSync( - cursor = result.cursor, - since = since, - count = totalCount, - startTime = startTime, - isInitialBatch = false - ) - } else { - datastoreRepo.putString(DatastoreKeys.libraryLastSyncTimestamp, startTime) + withContext(Dispatchers.IO) { + performItemSync( + cursor = null, + since = lastSyncDate.toString(), + count = 0, + startTime = syncStart.toString() + ) + CoroutineScope(Dispatchers.Main).launch { + isRefreshing = false + } + } } - } - override fun handleSavedItemAction(itemID: String, action: SavedItemAction) { - when (action) { - SavedItemAction.Delete -> { - deleteSavedItem(itemID) - } - SavedItemAction.Archive -> { - archiveSavedItem(itemID) - } - SavedItemAction.Unarchive -> { - unarchiveSavedItem(itemID) - } - SavedItemAction.EditLabels -> { - currentItemLiveData.value = itemID - showLabelsSelectionSheetLiveData.value = true - } - SavedItemAction.EditInfo -> { - currentItemLiveData.value = itemID - showEditInfoSheetLiveData.value = true - } - } - actionsMenuItemLiveData.postValue(null) - } + private suspend fun performItemSync( + cursor: String?, + since: String, + count: Int, + startTime: String, + isInitialBatch: Boolean = true + ) { + dataService.syncOfflineItemsWithServerIfNeeded() + val result = dataService.sync(since = since, cursor = cursor, limit = 20) - fun deleteSavedItem(itemID: String) { - viewModelScope.launch { - dataService.deleteSavedItem(itemID) - } - } + // Fetch content for the initial batch only + if (isInitialBatch) { + for (slug in result.savedItemSlugs) { + delay(250) + contentRequestChannel.send(slug) + } + } - fun archiveSavedItem(itemID: String) { - viewModelScope.launch { - dataService.archiveSavedItem(itemID) - } - } + val totalCount = count + result.count - fun unarchiveSavedItem(itemID: String) { - viewModelScope.launch { - dataService.unarchiveSavedItem(itemID) - } - } - - fun updateSavedItemLabels(savedItemID: String, labels: List) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - val result = setSavedItemLabels( - networker = networker, - dataService = dataService, - savedItemID = savedItemID, - labels = labels - ) - - snackbarMessage = if (result) { - resourceProvider.getString(R.string.library_view_model_snackbar_success) + if (!result.hasError && result.hasMoreItems && result.cursor != null) { + performItemSync( + cursor = result.cursor, + since = since, + count = totalCount, + startTime = startTime, + isInitialBatch = false + ) } else { - resourceProvider.getString(R.string.library_view_model_snackbar_error) + datastoreRepo.putString(DatastoreKeys.libraryLastSyncTimestamp, startTime) + } + } + + override fun handleSavedItemAction(itemID: String, action: SavedItemAction) { + when (action) { + SavedItemAction.Delete -> { + deleteSavedItem(itemID) + } + + SavedItemAction.Archive -> { + archiveSavedItem(itemID) + } + + SavedItemAction.Unarchive -> { + unarchiveSavedItem(itemID) + } + + SavedItemAction.EditLabels -> { + currentItemLiveData.value = itemID + bottomSheetState.value = LibraryBottomSheetState.EDIT + } + + SavedItemAction.EditInfo -> { + currentItemLiveData.value = itemID + bottomSheetState.value = LibraryBottomSheetState.EDIT + } + } + actionsMenuItemLiveData.postValue(null) + } + + fun deleteSavedItem(itemID: String) { + viewModelScope.launch { + dataService.deleteSavedItem(itemID) + } + } + + fun archiveSavedItem(itemID: String) { + viewModelScope.launch { + dataService.archiveSavedItem(itemID) + } + } + + fun unarchiveSavedItem(itemID: String) { + viewModelScope.launch { + dataService.unarchiveSavedItem(itemID) + } + } + + fun updateSavedItemLabels(savedItemID: String, labels: List) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val result = setSavedItemLabels( + networker = networker, + dataService = dataService, + savedItemID = savedItemID, + labels = labels + ) + + snackbarMessage = if (result) { + resourceProvider.getString(R.string.library_view_model_snackbar_success) + } else { + resourceProvider.getString(R.string.library_view_model_snackbar_error) + } + + CoroutineScope(Dispatchers.Main).launch { + handleFilterChanges() + } + } + } + } + + fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val newLabel = networker.createNewLabel( + CreateLabelInput( + color = Optional.presentIfNotNull(hexColorValue), name = labelName + ) + ) + + newLabel?.let { + val savedItemLabel = SavedItemLabel( + savedItemLabelId = it.id, + name = it.name, + color = it.color, + createdAt = it.createdAt as String?, + labelDescription = it.description + ) + + dataService.db.savedItemLabelDao().insertAll(listOf(savedItemLabel)) + } + } + } + } + + fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? { + currentItemLiveData.value?.let { itemID -> + return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID } } - CoroutineScope(Dispatchers.Main).launch { - handleFilterChanges() + return null + } + + private fun searchQueryString(): String { + var query = + "${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}" + + activeLabelsLiveData.value?.let { + if (it.isNotEmpty()) { + query += " label:" + query += it.joinToString { label -> label.name } + } } - } + + return query } - } - - fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - val newLabel = networker.createNewLabel(CreateLabelInput(color = Optional.presentIfNotNull(hexColorValue), name = labelName)) - - newLabel?.let { - val savedItemLabel = SavedItemLabel( - savedItemLabelId = it.id, - name = it.name, - color = it.color, - createdAt = it.createdAt as String?, - labelDescription = it.description - ) - - dataService.db.savedItemLabelDao().insertAll(listOf(savedItemLabel)) - } - } - } - } - - fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? { - currentItemLiveData.value?.let { itemID -> - return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID } - } - - return null - } - - private fun searchQueryString(): String { - var query = "${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}" - - activeLabelsLiveData.value?.let { - if (it.isNotEmpty()) { - query += " label:" - query += it.joinToString { label -> label.name } - } - } - - return query - } } enum class SavedItemAction { - Delete, - Archive, - Unarchive, - EditLabels, - EditInfo, + Delete, + Archive, + Unarchive, + EditLabels, + EditInfo, }