From 653955b3b845388213aa75a03f80c746353f9976 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Apr 2023 14:45:37 +0800 Subject: [PATCH 01/24] Set email and password keyboard types on login form This prevents auto complete and capitalization issues when logging in. --- .../java/app/omnivore/omnivore/ui/auth/EmailLogin.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt index bc3c1056a..87612a4b3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp @@ -113,7 +114,10 @@ fun LoginFields( placeholder = { Text(text = "user@email.com") }, label = { Text(text = "Email") }, onValueChange = onEmailChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Email, + ), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) ) @@ -123,7 +127,10 @@ fun LoginFields( label = { Text(text = "Password") }, onValueChange = onPasswordChange, visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Password, + ), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) ) From d2b99399bf418825a98fad80a2ae1e4c16b9a6fa Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Apr 2023 15:49:40 +0800 Subject: [PATCH 02/24] Dont set bottom padding on Android list view --- .../main/java/app/omnivore/omnivore/ui/library/LibraryView.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 70804fc19..bfba61f9e 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 @@ -47,8 +47,7 @@ fun LibraryView( libraryViewModel, modifier = Modifier .padding( - top = paddingValues.calculateTopPadding(), - bottom = paddingValues.calculateBottomPadding() + top = paddingValues.calculateTopPadding() ) ) } From 70777d323e62a78605fb4dd8fc34ed3d3bb89a88 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Apr 2023 16:05:29 +0800 Subject: [PATCH 03/24] Start to get cards closer to web design --- .../omnivore/ui/savedItemViews/SavedItemCard.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index ee98ec1f2..a7decb078 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -13,16 +13,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.components.LabelChipColors import app.omnivore.omnivore.ui.library.SavedItemAction import coil.compose.rememberAsyncImagePainter -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, +) @Composable fun SavedItemCard(cardData: SavedItemCardData, labels: List, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { var isMenuExpanded by remember { mutableStateOf(false) } @@ -52,7 +54,11 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC ) { Text( text = cardData.title, - style = MaterialTheme.typography.titleMedium, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold + ), + maxLines = 2, lineHeight = 20.sp ) From 9b0941d7055cbbdf59d87f2cde36d47d1dec7bcb Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 18 Apr 2023 23:21:55 +0800 Subject: [PATCH 04/24] Add a new SearchView that does typeahead search, add back button to WebReader The end goal here is to separate search and typeahead search so I can bring more properties onto the main search cards like save time, number of highlights, article length. --- .../main/java/app/omnivore/omnivore/Routes.kt | 1 + .../omnivore/ui/library/LibraryView.kt | 1 + .../omnivore/omnivore/ui/library/SearchBar.kt | 76 +++++++------ .../omnivore/ui/library/SearchView.kt | 100 ++++++++++++++++++ .../ui/reader/WebReaderLoadingContainer.kt | 18 +++- .../app/omnivore/omnivore/ui/root/RootView.kt | 8 ++ .../ui/savedItemViews/SavedItemCard.kt | 47 +++++--- 7 files changed, 193 insertions(+), 58 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt index 0ab373cc9..6608a30f6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt @@ -3,6 +3,7 @@ package app.omnivore.omnivore sealed class Routes(val route: String) { object Library : Routes("Library") object Settings: Routes("Settings") + object Search: Routes("Search") object Documentation: Routes("Documentation") object PrivacyPolicy: Routes("PrivacyPolicy") object TermsAndConditions: Routes("TermsAndConditions") 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 bfba61f9e..932168f76 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 @@ -39,6 +39,7 @@ fun LibraryView( topBar = { SearchBar( libraryViewModel = libraryViewModel, + onSearchClicked = { navController.navigate(Routes.Search.route) }, onSettingsIconClick = { navController.navigate(Routes.Settings.route) } ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index ca7ff4ff2..e55250c39 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -4,9 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -31,6 +29,7 @@ import app.omnivore.omnivore.R @Composable fun SearchBar( libraryViewModel: LibraryViewModel, + onSearchClicked: () -> Unit, onSettingsIconClick: () -> Unit ) { val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("") @@ -58,7 +57,7 @@ fun SearchBar( .padding(horizontal = 6.dp) ) } else { - IconButton(onClick = { libraryViewModel.showSearchField = true }) { + IconButton(onClick = onSearchClicked) { Icon( imageVector = Icons.Filled.Search, contentDescription = null @@ -87,44 +86,41 @@ fun SearchField( val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } - Row { - TextField( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 2.dp) - .onFocusChanged { focusState -> - showClearButton = (focusState.isFocused) - } - .focusRequester(focusRequester), - value = searchText, - onValueChange = onSearchTextChanged, - placeholder = { - Text(text = "Search") - }, - trailingIcon = { - AnimatedVisibility( - visible = showClearButton, - enter = fadeIn(), - exit = fadeOut() - ) { - IconButton(onClick = { onSearchTextChanged("") }) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = null - ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp) + .onFocusChanged { focusState -> + showClearButton = (focusState.isFocused) } + .focusRequester(focusRequester), + value = searchText, + onValueChange = onSearchTextChanged, + placeholder = { + Text(text = "Search") + }, + trailingIcon = { + AnimatedVisibility( + visible = showClearButton, + enter = fadeIn(), + exit = fadeOut() + ) { + IconButton(onClick = { onSearchTextChanged("") }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = null + ) + } - } - }, - maxLines = 1, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - ) - } - + } + }, + maxLines = 1, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + keyboardController?.hide() + }), + ) LaunchedEffect(Unit) { focusRequester.requestFocus() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt new file mode 100644 index 000000000..e110c9049 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -0,0 +1,100 @@ +package app.omnivore.omnivore.ui.library + +import android.content.Intent +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +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.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import app.omnivore.omnivore.Routes +import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels +import app.omnivore.omnivore.ui.components.LabelsSelectionSheet +import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard +import app.omnivore.omnivore.ui.reader.PDFReaderActivity +import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity +import kotlinx.coroutines.flow.distinctUntilChanged +import androidx.compose.ui.res.stringResource +import app.omnivore.omnivore.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchView( + libraryViewModel: LibraryViewModel, + navController: NavHostController +) { + val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("") + + Scaffold( + topBar = { + TopAppBar(title = { + SearchField(searchText) { libraryViewModel.updateSearchText(it) } + }, navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, + modifier = Modifier, + contentDescription = "Back" + ) + } + }) + } + ) { paddingValues -> + SearchViewContent( + libraryViewModel, + modifier = Modifier + .padding(vertical = paddingValues.calculateTopPadding()) + .background(Color.Blue) + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SearchViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { + val context = LocalContext.current + val listState = rememberLazyListState() + + val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) + + + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + + ) { + items(searchedCardsData) { cardDataWithLabels -> + SavedItemCard( + cardData = cardDataWithLabels.cardData, + labels = cardDataWithLabels.labels, + onClickHandler = { + val activityClass = if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + val intent = Intent(context, activityClass) + intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug) + context.startActivity(intent) + }, + actionHandler = { libraryViewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) } + ) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt index 2fd99ba6e..53fa6c433 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderLoadingContainer.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Settings @@ -32,6 +33,8 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController import app.omnivore.omnivore.MainActivity import app.omnivore.omnivore.R import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet @@ -40,6 +43,7 @@ import app.omnivore.omnivore.ui.theme.OmnivoreTheme import com.google.accompanist.systemuicontroller.rememberSystemUiController import dagger.hilt.android.AndroidEntryPoint import kotlin.math.roundToInt +import androidx.navigation.compose.rememberNavController @AndroidEntryPoint @@ -51,6 +55,7 @@ class WebReaderLoadingContainerActivity: ComponentActivity() { val requestID = intent.getStringExtra("SAVED_ITEM_REQUEST_ID") val slug = intent.getStringExtra("SAVED_ITEM_SLUG") + setContent { val systemUiController = rememberSystemUiController() val useDarkIcons = !isSystemInDarkTheme() @@ -77,7 +82,7 @@ class WebReaderLoadingContainerActivity: ComponentActivity() { requestID = requestID, slug = slug, onLibraryIconTap = if (requestID != null) { { startMainActivity() } } else null, - webReaderViewModel = viewModel + webReaderViewModel = viewModel, ) } } @@ -137,6 +142,17 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o }), backgroundColor = MaterialTheme.colorScheme.surfaceVariant, title = {}, + navigationIcon = { + IconButton(onClick = { + onBackPressedDispatcher?.onBackPressed() + }) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, + modifier = Modifier, + contentDescription = "Back" + ) + } + }, actions = { if (onLibraryIconTap != null) { IconButton(onClick = { onLibraryIconTap() }) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt index 01e5ffb26..64b81ef36 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt @@ -17,6 +17,7 @@ import app.omnivore.omnivore.Routes import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.auth.WelcomeScreen import app.omnivore.omnivore.ui.library.LibraryView +import app.omnivore.omnivore.ui.library.SearchView import app.omnivore.omnivore.ui.library.LibraryViewModel import app.omnivore.omnivore.ui.settings.PolicyWebView import app.omnivore.omnivore.ui.settings.SettingsViewModel @@ -80,6 +81,13 @@ fun PrimaryNavigator( ) } + composable(Routes.Search.route) { + SearchView( + libraryViewModel = libraryViewModel, + navController = navController + ) + } + composable(Routes.Settings.route) { SettingsView(loginViewModel = loginViewModel, settingsViewModel = settingsViewModel, navController = navController) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index a7decb078..ff85bfa7b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -43,14 +43,15 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC verticalAlignment = Alignment.Top, modifier = Modifier .fillMaxWidth() - .padding(12.dp) + .padding(15.dp) .background(if (isMenuExpanded) Color.LightGray else Color.Transparent) ) { Column( - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier .weight(1f, fill = false) - .padding(end = 8.dp) + .padding(end = 20.dp) + .defaultMinSize(minHeight = 55.dp) ) { Text( text = cardData.title, @@ -64,21 +65,21 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC if (cardData.author != null && cardData.author != "") { Text( - text = "By ${cardData.author}", - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - - if (publisherDisplayName != null) { - Text( - text = publisherDisplayName, + text = byline(cardData), style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis ) } +// +// if (publisherDisplayName != null) { +// Text( +// text = publisherDisplayName, +// style = MaterialTheme.typography.bodyMedium, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis +// ) +// } } if (cardData.imageURLString != null) { @@ -86,9 +87,8 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC painter = rememberAsyncImagePainter(cardData.imageURLString), contentDescription = "Image associated with saved item", modifier = Modifier - .padding(top = 6.dp) - .clip(RoundedCornerShape(6.dp)) - .size(80.dp) + .size(55.dp, 73.dp) + .clip(RoundedCornerShape(4.dp)) ) } } @@ -98,7 +98,7 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(start = 6.dp) + .padding(start = 5.dp) ) { items(labels.sortedBy { it.name }) { label -> val chipColors = LabelChipColors.fromHex(label.color) @@ -128,3 +128,16 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC ) } } + +fun byline(item: SavedItemCardData): String { + item.author?.let { + return item.author + } + + val publisherDisplayName = item.publisherDisplayName() + publisherDisplayName?.let { + return publisherDisplayName + } + + return "" +} From 1610ae4930a0c096fda56661631d4a88033521f4 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Apr 2023 12:21:06 +0800 Subject: [PATCH 05/24] Use a standalone search view for the Library --- .../omnivore/ui/library/LibraryView.kt | 8 ++- .../omnivore/ui/library/LibraryViewModel.kt | 1 - .../omnivore/omnivore/ui/library/SearchBar.kt | 53 +++++++++---------- .../omnivore/ui/library/SearchView.kt | 35 ++++++++---- 4 files changed, 53 insertions(+), 44 deletions(-) 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 932168f76..91e81f4a2 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 @@ -82,12 +82,10 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { .fillMaxSize() .padding(horizontal = 6.dp) ) { - if (!libraryViewModel.showSearchField) { - item { - LibraryFilterBar(libraryViewModel) - } + item { + LibraryFilterBar(libraryViewModel) } - items(if (libraryViewModel.showSearchField) searchedCardsData else cardsData) { cardDataWithLabels -> + items(cardsData) { cardDataWithLabels -> SavedItemCard( cardData = cardDataWithLabels.cardData, labels = cardDataWithLabels.labels, 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 012615283..d562fa63d 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 @@ -49,7 +49,6 @@ class LibraryViewModel @Inject constructor( val activeLabelsLiveData = MutableLiveData>(listOf()) var isRefreshing by mutableStateOf(false) - var showSearchField by mutableStateOf(false) var hasLoadedInitialFilters = false fun loadInitialFilterValues() { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index e55250c39..85b26488f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings @@ -23,6 +24,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController import app.omnivore.omnivore.R @OptIn(ExperimentalMaterial3Api::class) @@ -36,40 +38,24 @@ fun SearchBar( TopAppBar( title = { - if (libraryViewModel.showSearchField) { - SearchField(searchText) { libraryViewModel.updateSearchText(it) } - } else { Text("Library") - } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surfaceVariant ), actions = { - if (libraryViewModel.showSearchField) { - Text( - text = "Cancel", - modifier = Modifier - .clickable { - libraryViewModel.updateSearchText("") - libraryViewModel.showSearchField = false - } - .padding(horizontal = 6.dp) + IconButton(onClick = onSearchClicked) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null ) - } else { - IconButton(onClick = onSearchClicked) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = null - ) - } + } - IconButton(onClick = onSettingsIconClick) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = null - ) - } + IconButton(onClick = onSettingsIconClick) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = null + ) } } ) @@ -80,7 +66,8 @@ fun SearchBar( @Composable fun SearchField( searchText: String, - onSearchTextChanged: (String) -> Unit + onSearchTextChanged: (String) -> Unit, + navController: NavHostController, ) { var showClearButton by remember { mutableStateOf(false) } val keyboardController = LocalSoftwareKeyboardController.current @@ -89,7 +76,7 @@ fun SearchField( TextField( modifier = Modifier .fillMaxWidth() - .padding(vertical = 2.dp) + // .padding(vertical = 2.dp) .onFocusChanged { focusState -> showClearButton = (focusState.isFocused) } @@ -99,6 +86,16 @@ fun SearchField( placeholder = { Text(text = "Search") }, + leadingIcon = { + IconButton( + onClick = { navController.popBackStack() } + ) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, trailingIcon = { AnimatedVisibility( visible = showClearButton, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt index e110c9049..5f48004e5 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -43,17 +43,32 @@ fun SearchView( Scaffold( topBar = { - TopAppBar(title = { - SearchField(searchText) { libraryViewModel.updateSearchText(it) } - }, navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, - modifier = Modifier, - contentDescription = "Back" - ) + TopAppBar( + title = { Text("") }, +// SearchField(searchText) { libraryViewModel.updateSearchText(it) } +// }, +// navigationIcon = { +// IconButton( +// modifier = Modifier.background(color = Color.Red), +// onClick = { navController.popBackStack() } +// ) { +// Icon( +// imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, +// contentDescription = "Back" +// ) +// }, + + actions = { + Row { + SearchField( + searchText, + onSearchTextChanged = { libraryViewModel.updateSearchText(it) }, + navController = navController + ) + } } - }) + ) + } ) { paddingValues -> SearchViewContent( From f3019b0a6e4028681636aa9dad5ee6fd8a47bdcc Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Apr 2023 12:25:00 +0800 Subject: [PATCH 06/24] Clear search text when navigating back --- .../app/omnivore/omnivore/ui/library/SearchBar.kt | 14 ++++++++------ .../app/omnivore/omnivore/ui/library/SearchView.kt | 13 ------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index 85b26488f..20be1f3dd 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -73,7 +73,7 @@ fun SearchField( val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } - TextField( + TextField( modifier = Modifier .fillMaxWidth() // .padding(vertical = 2.dp) @@ -86,16 +86,18 @@ fun SearchField( placeholder = { Text(text = "Search") }, - leadingIcon = { - IconButton( - onClick = { navController.popBackStack() } - ) { + leadingIcon = { + IconButton( + onClick = { + onSearchTextChanged("") + navController.popBackStack() + }) { Icon( imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, contentDescription = "Back" ) } - }, + }, trailingIcon = { AnimatedVisibility( visible = showClearButton, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt index 5f48004e5..fb2263480 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -45,19 +45,6 @@ fun SearchView( topBar = { TopAppBar( title = { Text("") }, -// SearchField(searchText) { libraryViewModel.updateSearchText(it) } -// }, -// navigationIcon = { -// IconButton( -// modifier = Modifier.background(color = Color.Red), -// onClick = { navController.popBackStack() } -// ) { -// Icon( -// imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, -// contentDescription = "Back" -// ) -// }, - actions = { Row { SearchField( From 5261d054d486ff84c56346ab28a876c5f478571d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Apr 2023 13:01:22 +0800 Subject: [PATCH 07/24] Start to separate typeahead search from the view model --- .../omnivore/ui/library/LibraryView.kt | 1 - .../omnivore/ui/library/LibraryViewModel.kt | 18 +++++++----------- .../omnivore/omnivore/ui/library/SearchBar.kt | 9 ++++----- .../omnivore/omnivore/ui/library/SearchView.kt | 4 ++++ 4 files changed, 15 insertions(+), 17 deletions(-) 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 91e81f4a2..20fb83f8a 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 @@ -66,7 +66,6 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { ) val cardsData: List by libraryViewModel.itemsLiveData.observeAsState(listOf()) - val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) Box( modifier = Modifier 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 d562fa63d..c09469b2c 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 @@ -97,7 +97,9 @@ class LibraryViewModel @Inject constructor( if (text == "") { searchItemsLiveData.value = listOf() } else { - load(clearPreviousSearch = true) + viewModelScope.launch { + performTypeaheadSearch(true) + } } } @@ -133,12 +135,8 @@ class LibraryViewModel @Inject constructor( loadInitialFilterValues() viewModelScope.launch { - if (searchTextLiveData.value != "") { - performSearch(clearPreviousSearch) - } else { - syncItems() - loadUsingSearchAPI() - } + syncItems() + loadUsingSearchAPI() } } @@ -189,9 +187,7 @@ class LibraryViewModel @Inject constructor( } suspend fun handleFilterChanges() { - if (searchTextLiveData.value != "") { - performSearch(true) - } else if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { + if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { itemsLiveDataInternal = dataService.libraryLiveData(appliedFilterLiveData.value!!, appliedSortFilterLiveData.value!!, activeLabelsLiveData.value ?: listOf()) itemsLiveData.removeSource(itemsLiveDataInternal) itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue) @@ -237,7 +233,7 @@ class LibraryViewModel @Inject constructor( } } - private suspend fun performSearch(clearPreviousSearch: Boolean) { + private suspend fun performTypeaheadSearch(clearPreviousSearch: Boolean) { if (clearPreviousSearch) { cursor = null } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index 20be1f3dd..cb3d0a149 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -66,17 +66,16 @@ fun SearchBar( @Composable fun SearchField( searchText: String, + onSearch: () -> Unit, onSearchTextChanged: (String) -> Unit, navController: NavHostController, ) { var showClearButton by remember { mutableStateOf(false) } - val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } TextField( modifier = Modifier .fillMaxWidth() - // .padding(vertical = 2.dp) .onFocusChanged { focusState -> showClearButton = (focusState.isFocused) } @@ -115,9 +114,9 @@ fun SearchField( }, maxLines = 1, singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { + onSearch() }), ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt index fb2263480..570ce6b7a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -49,6 +49,10 @@ fun SearchView( Row { SearchField( searchText, + onSearch = { + libraryViewModel.loadUsingSearchAPI() + navController.popBackStack() + }, onSearchTextChanged = { libraryViewModel.updateSearchText(it) }, navController = navController ) From 51c4fcaece274059b8f9c11821311b6d3342d093 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Apr 2023 18:11:46 +0800 Subject: [PATCH 08/24] WIP: transition to typeahead --- .../omnivore/dataService/LibrarySync.kt | 20 ++++- .../omnivore/networking/SearchQuery.kt | 10 +-- .../persistence/entities/SavedItem.kt | 7 ++ .../omnivore/ui/library/LibraryView.kt | 3 +- .../omnivore/ui/library/LibraryViewModel.kt | 84 +++++++++++++++++-- .../omnivore/ui/library/SearchView.kt | 25 +++--- .../ui/savedItemViews/TypeaheadSearchCard.kt | 67 +++++++++++++++ 7 files changed, 182 insertions(+), 34 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/TypeaheadSearchCard.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index 43fffe011..a5f558f9f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -4,7 +4,7 @@ import android.util.Log import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.* -suspend fun DataService.librarySearch(cursor: String?, query: String): SavedItemSyncResult { +suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult { val searchResult = networker.search(cursor = cursor, limit = 10, query = query) val savedItems = searchResult.items.map { it.item } @@ -28,14 +28,14 @@ suspend fun DataService.librarySearch(cursor: String?, query: String): SavedItem db.savedItemLabelDao().insertAll(labels) db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) - Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query") + Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor") - return SavedItemSyncResult( + return SearchResult( hasError = false, hasMoreItems = false, cursor = searchResult.cursor, count = searchResult.items.size, - savedItemSlugs = savedItems.map { it.slug } + savedItems = savedItems ) } @@ -150,3 +150,15 @@ data class SavedItemSyncResult( val errorResult = SavedItemSyncResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItemSlugs = listOf()) } } + +data class SearchResult( + val hasError: Boolean, + val hasMoreItems: Boolean, + val count: Int, + val savedItems: List, + val cursor: String? +) { + companion object { + val errorResult = SearchResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItems = listOf()) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt index a1706493e..5a1811474 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt @@ -5,11 +5,12 @@ import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery import app.omnivore.omnivore.persistence.entities.SavedItem import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.persistence.entities.TypeaheadCardData import com.apollographql.apollo3.api.Optional data class SearchQueryResponse( val cursor: String?, - val cardsData: List + val cardsData: List ) data class LibrarySearchQueryResponse( @@ -33,16 +34,11 @@ suspend fun Networker.typeaheadSearch( val itemList = result.data?.typeaheadSearch?.onTypeaheadSearchSuccess?.items ?: listOf() val cardsData = itemList.map { - SavedItemCardData( + TypeaheadCardData( savedItemId = it.id, slug = it.slug, - publisherURLString = "", title = it.title, - author = "", - imageURLString = null, isArchived = false, - pageURLString = "", - contentReader = null, ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 548adffb9..4ed24c0e1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -87,6 +87,13 @@ data class SavedItemCardData( } } +data class TypeaheadCardData( + val savedItemId: String, + val slug: String, + val title: String, + val isArchived: Boolean, +) + @Dao interface SavedItemDao { @Query("SELECT * FROM savedItem") 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 20fb83f8a..3942f4eae 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 @@ -100,7 +100,8 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { } InfiniteListHandler(listState = listState) { - if (cardsData.isEmpty()) { + val searchText = libraryViewModel.searchTextLiveData.value ?: "" + if (cardsData.isEmpty() && searchText.isEmpty()) { Log.d("sync", "loading with load func") libraryViewModel.initialLoad() } else { 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 c09469b2c..0f4e37832 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 @@ -38,7 +38,7 @@ class LibraryViewModel @Inject constructor( // Live Data val searchTextLiveData = MutableLiveData("") - val searchItemsLiveData = MutableLiveData>(listOf()) + val searchItemsLiveData = MutableLiveData>(listOf()) private var itemsLiveDataInternal = dataService.libraryLiveData(SavedItemFilter.INBOX, SavedItemSortFilter.NEWEST, listOf()) val itemsLiveData = MediatorLiveData>() val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) @@ -103,6 +103,53 @@ class LibraryViewModel @Inject constructor( } } + fun performSearch() { + // To perform search we just clear the current state, so the LibraryView infinite scroll + // load will update items. + viewModelScope.launch { + itemsLiveData.postValue(listOf()) + librarySearchCursor = null + + 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.slug) + + if (!isSavedInDB) { + delay(2000) + contentRequestChannel.send(it.slug) + } + } + + val newItems = result.savedItems.map { + SavedItemCardDataWithLabels( + cardData = SavedItemCardData( + savedItemId = it.savedItemId, + slug = it.slug, + publisherURLString = it.publisherURLString, + title = it.title, + author = it.author, + imageURLString = it.imageURLString, + isArchived = it.isArchived, + pageURLString = it.pageURLString, + contentReader = it.contentReader + ), + labels = listOf() + ) + } + + itemsLiveData.postValue(newItems) + } + } + } + fun refresh() { isRefreshing = true load(true) @@ -151,14 +198,37 @@ class LibraryViewModel @Inject constructor( isRefreshing = false } - result.savedItemSlugs.map { - val isSavedInDB = dataService.isSavedItemContentStoredInDB(it) + result.savedItems.map { + val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.slug) if (!isSavedInDB) { delay(2000) - contentRequestChannel.send(it) + contentRequestChannel.send(it.slug) } } + + val newItems = result.savedItems.map { + SavedItemCardDataWithLabels( + cardData = SavedItemCardData( + savedItemId = it.savedItemId, + slug = it.slug, + publisherURLString = it.publisherURLString, + title = it.title, + author = it.author, + imageURLString = it.imageURLString, + isArchived = it.isArchived, + pageURLString = it.pageURLString, + contentReader = it.contentReader + ), + labels = listOf() + ) + } + + itemsLiveData.value?.let{ + itemsLiveData.postValue(newItems + it) + } ?: run { + itemsLiveData.postValue(newItems) + } } } } @@ -253,11 +323,7 @@ class LibraryViewModel @Inject constructor( return } - val cardsDataWithLabels = searchResult.cardsData.map { - SavedItemCardDataWithLabels(cardData = it, labels = listOf()) - } - - searchItemsLiveData.postValue(cardsDataWithLabels) + searchItemsLiveData.postValue(searchResult.cardsData) CoroutineScope(Dispatchers.Main).launch { isRefreshing = false diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt index 570ce6b7a..264f6017b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -32,6 +32,8 @@ import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity import kotlinx.coroutines.flow.distinctUntilChanged import androidx.compose.ui.res.stringResource import app.omnivore.omnivore.R +import app.omnivore.omnivore.persistence.entities.TypeaheadCardData +import app.omnivore.omnivore.ui.savedItemViews.TypeaheadSearchCard @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -50,7 +52,7 @@ fun SearchView( SearchField( searchText, onSearch = { - libraryViewModel.loadUsingSearchAPI() + libraryViewModel.performSearch() navController.popBackStack() }, onSearchTextChanged = { libraryViewModel.updateSearchText(it) }, @@ -65,8 +67,7 @@ fun SearchView( SearchViewContent( libraryViewModel, modifier = Modifier - .padding(vertical = paddingValues.calculateTopPadding()) - .background(Color.Blue) + .padding(top = paddingValues.calculateTopPadding()) ) } } @@ -76,9 +77,7 @@ fun SearchView( fun SearchViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { val context = LocalContext.current val listState = rememberLazyListState() - - val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) - + val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) LazyColumn( state = listState, @@ -89,17 +88,17 @@ fun SearchViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { .fillMaxSize() ) { - items(searchedCardsData) { cardDataWithLabels -> - SavedItemCard( - cardData = cardDataWithLabels.cardData, - labels = cardDataWithLabels.labels, + items(searchedCardsData) { cardData -> + TypeaheadSearchCard( + cardData = cardData, onClickHandler = { - val activityClass = if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + // val activityClass = if (cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + val activityClass = WebReaderLoadingContainerActivity::class.java val intent = Intent(context, activityClass) - intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug) + intent.putExtra("SAVED_ITEM_SLUG", cardData.slug) context.startActivity(intent) }, - actionHandler = { libraryViewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) } + actionHandler = { libraryViewModel.handleSavedItemAction(cardData.savedItemId, it) } ) } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/TypeaheadSearchCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/TypeaheadSearchCard.kt new file mode 100644 index 000000000..337b8afae --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/TypeaheadSearchCard.kt @@ -0,0 +1,67 @@ +package app.omnivore.omnivore.ui.savedItemViews + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.material3.SuggestionChipDefaults.elevatedSuggestionChipColors +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.persistence.entities.TypeaheadCardData +import app.omnivore.omnivore.ui.components.LabelChipColors +import app.omnivore.omnivore.ui.library.SavedItemAction +import coil.compose.rememberAsyncImagePainter + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, +) +@Composable +fun TypeaheadSearchCard(cardData: TypeaheadCardData, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { + Column( + modifier = Modifier + .combinedClickable( + onClick = onClickHandler, + ) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .padding(15.dp) + .background(Color.Transparent) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(5.dp), + modifier = Modifier + .weight(1f, fill = false) + .padding(end = 20.dp) + .defaultMinSize(minHeight = 55.dp) + ) { + Text( + text = cardData.title, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold + ), + maxLines = 2, + lineHeight = 20.sp + ) + + } + } + + Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) + } +} From adfa67f896011a49388df2fc4cceb92cac458624 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Apr 2023 18:34:08 +0800 Subject: [PATCH 09/24] Load more data for saved items --- .../app/omnivore/omnivore/persistence/entities/SavedItem.kt | 6 ++++-- .../app/omnivore/omnivore/ui/library/LibraryViewModel.kt | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 4ed24c0e1..ae493ebc2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -75,7 +75,9 @@ data class SavedItemCardData( val imageURLString: String?, val isArchived: Boolean, val pageURLString: String, - val contentReader: String? + val contentReader: String?, + val savedAt: String, + val readingProgress: Double, ) { fun publisherDisplayName(): String? { return publisherURLString?.toUri()?.host @@ -159,5 +161,5 @@ interface SavedItemDao { object SavedItemQueryConstants { - const val columns = "savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader " + const val columns = "savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress" } 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 0f4e37832..a1c45a9ba 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 @@ -132,6 +132,8 @@ class LibraryViewModel @Inject constructor( SavedItemCardDataWithLabels( cardData = SavedItemCardData( savedItemId = it.savedItemId, + savedAt = it.savedAt, + readingProgress = it.readingProgress, slug = it.slug, publisherURLString = it.publisherURLString, title = it.title, @@ -218,7 +220,9 @@ class LibraryViewModel @Inject constructor( imageURLString = it.imageURLString, isArchived = it.isArchived, pageURLString = it.pageURLString, - contentReader = it.contentReader + contentReader = it.contentReader, + savedAt = it.savedAt, + readingProgress = it.readingProgress ), labels = listOf() ) From 2ff12a70ad3eb86db0747ee37ef925fbb2577a6d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 19 Apr 2023 21:11:38 +0800 Subject: [PATCH 10/24] Update GQL, fetch wordsCount --- .../src/main/graphql/ArticleContent.graphql | 1 + .../app/src/main/graphql/Search.graphql | 5 + .../app/src/main/graphql/UpdatesSince.graphql | 5 + .../app/src/main/graphql/schema.graphqls | 603 +++++++++++++++++- .../dataService/HighlightActionHandlers.kt | 4 +- .../omnivore/dataService/LibrarySync.kt | 3 +- .../dataService/SyncOfflineChanges.kt | 7 +- .../omnivore/networking/HighlightMutations.kt | 4 +- .../omnivore/networking/SavedItemQuery.kt | 3 +- .../omnivore/networking/SearchQuery.kt | 3 +- .../omnivore/persistence/AppDatabase.kt | 2 +- .../persistence/entities/Highlight.kt | 4 +- .../persistence/entities/SavedItem.kt | 6 +- .../omnivore/ui/library/LibraryViewModel.kt | 9 +- .../omnivore/omnivore/ui/reader/PDFReader.kt | 15 +- .../omnivore/ui/reader/PDFReaderViewModel.kt | 4 +- .../omnivore/ui/reader/WebReaderViewModel.kt | 5 +- .../ui/savedItemViews/SavedItemCard.kt | 103 ++- 18 files changed, 746 insertions(+), 40 deletions(-) diff --git a/android/Omnivore/app/src/main/graphql/ArticleContent.graphql b/android/Omnivore/app/src/main/graphql/ArticleContent.graphql index 350999bf7..ea8a8380f 100644 --- a/android/Omnivore/app/src/main/graphql/ArticleContent.graphql +++ b/android/Omnivore/app/src/main/graphql/ArticleContent.graphql @@ -40,6 +40,7 @@ fragment ArticleFields on Article { readAt updatedAt content + wordsCount } fragment HighlightFields on Highlight { diff --git a/android/Omnivore/app/src/main/graphql/Search.graphql b/android/Omnivore/app/src/main/graphql/Search.graphql index 7b231bafb..9a397ce81 100644 --- a/android/Omnivore/app/src/main/graphql/Search.graphql +++ b/android/Omnivore/app/src/main/graphql/Search.graphql @@ -26,6 +26,10 @@ query Search($after: String, $first: Int, $query: String) { name color } + highlights { + id + type + } pageId shortId quote @@ -36,6 +40,7 @@ query Search($after: String, $first: Int, $query: String) { readAt savedAt updatedAt + wordsCount } } pageInfo { diff --git a/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql index e7426f016..d1a7434bd 100644 --- a/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql +++ b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql @@ -28,6 +28,10 @@ query UpdatesSince($after: String, $first: Int, $since: Date!) { name color } + highlights { + id + type + } pageId shortId quote @@ -39,6 +43,7 @@ query UpdatesSince($after: String, $first: Int, $since: Date!) { savedAt updatedAt language + wordsCount } } pageInfo { diff --git a/android/Omnivore/app/src/main/graphql/schema.graphqls b/android/Omnivore/app/src/main/graphql/schema.graphqls index a0d0e1161..420960b75 100644 --- a/android/Omnivore/app/src/main/graphql/schema.graphqls +++ b/android/Omnivore/app/src/main/graphql/schema.graphqls @@ -1,4 +1,4 @@ -directive @sanitize(allowedTags: [String], maxLength: Int, pattern: String) on INPUT_FIELD_DEFINITION +directive @sanitize(allowedTags: [String], maxLength: Int, minLength: Int, pattern: String) on INPUT_FIELD_DEFINITION type AddPopularReadError { errorCodes: [AddPopularReadErrorCode!]! @@ -86,6 +86,8 @@ type Article { readAt: Date readingProgressAnchorIndex: Int! readingProgressPercent: Float! + readingProgressTopPercent: Float + recommendations: [Recommendation!] savedAt: Date! savedByViewer: Boolean shareInfo: LinkShareInfo @@ -101,6 +103,7 @@ type Article { updatedAt: Date! uploadFileId: ID url: String! + wordsCount: Int } type ArticleEdge { @@ -132,6 +135,7 @@ type ArticleSavingRequest { slug: String! status: ArticleSavingRequestStatus! updatedAt: Date! + url: String! user: User! userId: ID! @deprecated(reason: "userId has been replaced with user") } @@ -141,6 +145,7 @@ type ArticleSavingRequestError { } enum ArticleSavingRequestErrorCode { + BAD_DATA NOT_FOUND UNAUTHORIZED } @@ -148,6 +153,7 @@ enum ArticleSavingRequestErrorCode { union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequestSuccess enum ArticleSavingRequestStatus { + ARCHIVED DELETED FAILED PROCESSING @@ -177,7 +183,27 @@ type ArticlesSuccess { pageInfo: PageInfo! } +type BulkActionError { + errorCodes: [BulkActionErrorCode!]! +} + +enum BulkActionErrorCode { + UNAUTHORIZED +} + +union BulkActionResult = BulkActionError | BulkActionSuccess + +type BulkActionSuccess { + success: Boolean! +} + +enum BulkActionType { + ARCHIVE + DELETE +} + enum ContentReader { + EPUB PDF WEB } @@ -198,9 +224,11 @@ enum CreateArticleErrorCode { input CreateArticleInput { articleSavingRequestId: ID + labels: [CreateLabelInput!] preparedDocument: PreparedDocumentInput skipParsing: Boolean source: String + state: ArticleSavingRequestStatus uploadFileId: ID url: String! } @@ -232,6 +260,31 @@ type CreateArticleSuccess { user: User! } +type CreateGroupError { + errorCodes: [CreateGroupErrorCode!]! +} + +enum CreateGroupErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input CreateGroupInput { + description: String + expiresInDays: Int + maxMembers: Int + name: String! + onlyAdminCanPost: Boolean + onlyAdminCanSeeMembers: Boolean + topics: [String!] +} + +union CreateGroupResult = CreateGroupError | CreateGroupSuccess + +type CreateGroupSuccess { + group: RecommendationGroup! +} + type CreateHighlightError { errorCodes: [CreateHighlightErrorCode!]! } @@ -247,13 +300,17 @@ enum CreateHighlightErrorCode { input CreateHighlightInput { annotation: String articleId: ID! + highlightPositionAnchorIndex: Int + highlightPositionPercent: Float + html: String id: ID! - patch: String! + patch: String prefix: String - quote: String! + quote: String sharedAt: Date shortId: String! suffix: String + type: HighlightType } type CreateHighlightReplyError { @@ -296,7 +353,7 @@ enum CreateLabelErrorCode { } input CreateLabelInput { - color: String! + color: String description: String name: String! } @@ -388,6 +445,22 @@ type DeleteAccountSuccess { userID: ID! } +type DeleteFilterError { + errorCodes: [DeleteFilterErrorCode!]! +} + +enum DeleteFilterErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteFilterResult = DeleteFilterError | DeleteFilterSuccess + +type DeleteFilterSuccess { + filter: Filter! +} + type DeleteHighlightError { errorCodes: [DeleteHighlightErrorCode!]! } @@ -500,6 +573,22 @@ type DeleteReminderSuccess { reminder: Reminder! } +type DeleteRuleError { + errorCodes: [DeleteRuleErrorCode!]! +} + +enum DeleteRuleErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteRuleResult = DeleteRuleError | DeleteRuleSuccess + +type DeleteRuleSuccess { + rule: Rule! +} + type DeleteWebhookError { errorCodes: [DeleteWebhookErrorCode!]! } @@ -522,6 +611,31 @@ type DeviceToken { token: String! } +type DeviceTokensError { + errorCodes: [DeviceTokensErrorCode!]! +} + +enum DeviceTokensErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union DeviceTokensResult = DeviceTokensError | DeviceTokensSuccess + +type DeviceTokensSuccess { + deviceTokens: [DeviceToken!]! +} + +type Feature { + createdAt: Date! + expiresAt: Date + grantedAt: Date + id: ID! + name: String! + token: String! + updatedAt: Date! +} + type FeedArticle { annotationsCount: Int article: Article! @@ -555,6 +669,31 @@ type FeedArticlesSuccess { pageInfo: PageInfo! } +type Filter { + createdAt: Date! + description: String + filter: String! + id: ID! + name: String! + position: Int! + updatedAt: Date! +} + +type FiltersError { + errorCodes: [FiltersErrorCode!]! +} + +enum FiltersErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union FiltersResult = FiltersError | FiltersSuccess + +type FiltersSuccess { + filters: [Filter!]! +} + type GenerateApiKeyError { errorCodes: [GenerateApiKeyErrorCode!]! } @@ -644,19 +783,39 @@ type GoogleSignupSuccess { me: User! } +type GroupsError { + errorCodes: [GroupsErrorCode!]! +} + +enum GroupsErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union GroupsResult = GroupsError | GroupsSuccess + +type GroupsSuccess { + groups: [RecommendationGroup!]! +} + type Highlight { annotation: String createdAt: Date! createdByMe: Boolean! + highlightPositionAnchorIndex: Int + highlightPositionPercent: Float + html: String id: ID! - patch: String! + labels: [Label!] + patch: String prefix: String - quote: String! + quote: String reactions: [Reaction!]! replies: [HighlightReply!]! sharedAt: Date shortId: String! suffix: String + type: HighlightType! updatedAt: Date! user: User! } @@ -674,17 +833,40 @@ type HighlightStats { highlightCount: Int! } +enum HighlightType { + HIGHLIGHT + NOTE + REDACTION +} + +type ImportFromIntegrationError { + errorCodes: [ImportFromIntegrationErrorCode!]! +} + +enum ImportFromIntegrationErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union ImportFromIntegrationResult = ImportFromIntegrationError | ImportFromIntegrationSuccess + +type ImportFromIntegrationSuccess { + success: Boolean! +} + type Integration { createdAt: Date! enabled: Boolean! id: ID! + name: String! token: String! type: IntegrationType! updatedAt: Date! } enum IntegrationType { - READWISE + EXPORT + IMPORT } type IntegrationsError { @@ -702,6 +884,22 @@ type IntegrationsSuccess { integrations: [Integration!]! } +type JoinGroupError { + errorCodes: [JoinGroupErrorCode!]! +} + +enum JoinGroupErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union JoinGroupResult = JoinGroupError | JoinGroupSuccess + +type JoinGroupSuccess { + group: RecommendationGroup! +} + type Label { color: String! createdAt: Date @@ -727,6 +925,22 @@ type LabelsSuccess { labels: [Label!]! } +type LeaveGroupError { + errorCodes: [LeaveGroupErrorCode!]! +} + +enum LeaveGroupErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union LeaveGroupResult = LeaveGroupError | LeaveGroupSuccess + +type LeaveGroupSuccess { + success: Boolean! +} + type Link { highlightStats: HighlightStats! id: ID! @@ -782,6 +996,22 @@ type LoginSuccess { me: User! } +type MarkEmailAsItemError { + errorCodes: [MarkEmailAsItemErrorCode!]! +} + +enum MarkEmailAsItemErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union MarkEmailAsItemResult = MarkEmailAsItemError | MarkEmailAsItemSuccess + +type MarkEmailAsItemSuccess { + success: Boolean! +} + type MergeHighlightError { errorCodes: [MergeHighlightErrorCode!]! } @@ -797,6 +1027,9 @@ enum MergeHighlightErrorCode { input MergeHighlightInput { annotation: String articleId: ID! + highlightPositionAnchorIndex: Int + highlightPositionPercent: Float + html: String id: ID! overlapHighlightIdList: [String!]! patch: String! @@ -813,6 +1046,27 @@ type MergeHighlightSuccess { overlapHighlightIdList: [String!]! } +type MoveFilterError { + errorCodes: [MoveFilterErrorCode!]! +} + +enum MoveFilterErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input MoveFilterInput { + afterFilterId: ID + filterId: ID! +} + +union MoveFilterResult = MoveFilterError | MoveFilterSuccess + +type MoveFilterSuccess { + filter: Filter! +} + type MoveLabelError { errorCodes: [MoveLabelErrorCode!]! } @@ -836,8 +1090,10 @@ type MoveLabelSuccess { type Mutation { addPopularRead(name: String!): AddPopularReadResult! + bulkAction(action: BulkActionType!, query: String): BulkActionResult! createArticle(input: CreateArticleInput!): CreateArticleResult! createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult! + createGroup(input: CreateGroupInput!): CreateGroupResult! createHighlight(input: CreateHighlightInput!): CreateHighlightResult! createHighlightReply(input: CreateHighlightReplyInput!): CreateHighlightReplyResult! createLabel(input: CreateLabelInput!): CreateLabelResult! @@ -845,6 +1101,7 @@ type Mutation { createReaction(input: CreateReactionInput!): CreateReactionResult! createReminder(input: CreateReminderInput!): CreateReminderResult! deleteAccount(userID: ID!): DeleteAccountResult! + deleteFilter(id: ID!): DeleteFilterResult! deleteHighlight(highlightId: ID!): DeleteHighlightResult! deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult! deleteIntegration(id: ID!): DeleteIntegrationResult! @@ -852,17 +1109,27 @@ type Mutation { deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult! deleteReaction(id: ID!): DeleteReactionResult! deleteReminder(id: ID!): DeleteReminderResult! + deleteRule(id: ID!): DeleteRuleResult! deleteWebhook(id: ID!): DeleteWebhookResult! generateApiKey(input: GenerateApiKeyInput!): GenerateApiKeyResult! googleLogin(input: GoogleLoginInput!): LoginResult! googleSignup(input: GoogleSignupInput!): GoogleSignupResult! + importFromIntegration(integrationId: ID!): ImportFromIntegrationResult! + joinGroup(inviteCode: String!): JoinGroupResult! + leaveGroup(groupId: ID!): LeaveGroupResult! logOut: LogOutResult! + markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult! mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult! + moveFilter(input: MoveFilterInput!): MoveFilterResult! moveLabel(input: MoveLabelInput!): MoveLabelResult! + optInFeature(input: OptInFeatureInput!): OptInFeatureResult! + recommend(input: RecommendInput!): RecommendResult! + recommendHighlights(input: RecommendHighlightsInput!): RecommendHighlightsResult! reportItem(input: ReportItemInput!): ReportItemResult! revokeApiKey(id: ID!): RevokeApiKeyResult! saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! saveFile(input: SaveFileInput!): SaveResult! + saveFilter(input: SaveFilterInput!): SaveFilterResult! savePage(input: SavePageInput!): SaveResult! saveUrl(input: SaveUrlInput!): SaveResult! setBookmarkArticle(input: SetBookmarkArticleInput!): SetBookmarkArticleResult! @@ -872,6 +1139,7 @@ type Mutation { setLabels(input: SetLabelsInput!): SetLabelsResult! setLabelsForHighlight(input: SetLabelsForHighlightInput!): SetLabelsResult! setLinkArchived(input: ArchiveLinkInput!): ArchiveLinkResult! + setRule(input: SetRuleInput!): SetRuleResult! setShareArticle(input: SetShareArticleInput!): SetShareArticleResult! setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult! setUserPersonalization(input: SetUserPersonalizationInput!): SetUserPersonalizationResult! @@ -888,12 +1156,15 @@ type Mutation { updateUser(input: UpdateUserInput!): UpdateUserResult! updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResult! uploadFileRequest(input: UploadFileRequestInput!): UploadFileRequestResult! + uploadImportFile(contentType: String!, type: UploadImportFileType!): UploadImportFileResult! } type NewsletterEmail { address: String! confirmationCode: String + createdAt: Date! id: ID! + subscriptionCount: Int! } type NewsletterEmailsError { @@ -911,6 +1182,25 @@ type NewsletterEmailsSuccess { newsletterEmails: [NewsletterEmail!]! } +type OptInFeatureError { + errorCodes: [OptInFeatureErrorCode!]! +} + +enum OptInFeatureErrorCode { + BAD_REQUEST + NOT_FOUND +} + +input OptInFeatureInput { + name: String! +} + +union OptInFeatureResult = OptInFeatureError | OptInFeatureSuccess + +type OptInFeatureSuccess { + feature: Feature! +} + type Page { author: String createdAt: Date! @@ -951,11 +1241,29 @@ enum PageType { BOOK FILE HIGHLIGHTS + IMAGE PROFILE + TWEET UNKNOWN + VIDEO WEBSITE } +input ParseResult { + byline: String + content: String! + dir: String + excerpt: String! + language: String + length: Int! + previewImage: String + publishedDate: Date + siteIcon: String + siteName: String + textContent: String! + title: String! +} + input PreparedDocumentInput { document: String! pageInfo: PageInfoInput! @@ -971,25 +1279,31 @@ type Profile { type Query { apiKeys: ApiKeysResult! - article(slug: String!, username: String!): ArticleResult! - articleSavingRequest(id: ID!): ArticleSavingRequestResult! + article(format: String, slug: String!, username: String!): ArticleResult! + articleSavingRequest(id: ID, url: String): ArticleSavingRequestResult! articles(after: String, first: Int, includePending: Boolean, query: String, sharedOnly: Boolean, sort: SortParams): ArticlesResult! + deviceTokens: DeviceTokensResult! feedArticles(after: String, first: Int, sharedByUser: ID, sort: SortParams): FeedArticlesResult! + filters: FiltersResult! getFollowers(userId: ID): GetFollowersResult! getFollowing(userId: ID): GetFollowingResult! getUserPersonalization: GetUserPersonalizationResult! + groups: GroupsResult! hello: String integrations: IntegrationsResult! labels: LabelsResult! me: User newsletterEmails: NewsletterEmailsResult! + recentEmails: RecentEmailsResult! + recentSearches: RecentSearchesResult! reminder(linkId: ID!): ReminderResult! - search(after: String, first: Int, query: String): SearchResult! + rules(enabled: Boolean): RulesResult! + search(after: String, first: Int, format: String, includeContent: Boolean, query: String): SearchResult! sendInstallInstructions: SendInstallInstructionsResult! sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult! subscriptions(sort: SortParams): SubscriptionsResult! typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult! - updatesSince(after: String, first: Int, since: Date!): UpdatesSinceResult! + updatesSince(after: String, first: Int, since: Date!, sort: SortParams): UpdatesSinceResult! user(userId: ID, username: String): UserResult! users: UsersResult! validateUsername(username: String!): Boolean! @@ -1021,6 +1335,128 @@ type ReadState { readingTime: Int } +type RecentEmail { + createdAt: Date! + from: String! + html: String + id: ID! + subject: String! + text: String! + to: String! + type: String! +} + +type RecentEmailsError { + errorCodes: [RecentEmailsErrorCode!]! +} + +enum RecentEmailsErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union RecentEmailsResult = RecentEmailsError | RecentEmailsSuccess + +type RecentEmailsSuccess { + recentEmails: [RecentEmail!]! +} + +type RecentSearch { + createdAt: Date! + id: ID! + term: String! +} + +type RecentSearchesError { + errorCodes: [RecentSearchesErrorCode!]! +} + +enum RecentSearchesErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union RecentSearchesResult = RecentSearchesError | RecentSearchesSuccess + +type RecentSearchesSuccess { + searches: [RecentSearch!]! +} + +type RecommendError { + errorCodes: [RecommendErrorCode!]! +} + +enum RecommendErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +type RecommendHighlightsError { + errorCodes: [RecommendHighlightsErrorCode!]! +} + +enum RecommendHighlightsErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input RecommendHighlightsInput { + groupIds: [ID!]! + highlightIds: [ID!]! + note: String + pageId: ID! +} + +union RecommendHighlightsResult = RecommendHighlightsError | RecommendHighlightsSuccess + +type RecommendHighlightsSuccess { + success: Boolean! +} + +input RecommendInput { + groupIds: [ID!]! + note: String + pageId: ID! + recommendedWithHighlights: Boolean +} + +union RecommendResult = RecommendError | RecommendSuccess + +type RecommendSuccess { + success: Boolean! +} + +type Recommendation { + id: ID! + name: String! + note: String + recommendedAt: Date! + user: RecommendingUser +} + +type RecommendationGroup { + admins: [User!]! + canPost: Boolean! + canSeeMembers: Boolean! + createdAt: Date! + description: String + id: ID! + inviteUrl: String! + members: [User!]! + name: String! + topics: [String!] + updatedAt: Date! +} + +type RecommendingUser { + name: String! + profileImageURL: String + userId: String! + username: String! +} + type Reminder { archiveUntil: Boolean! id: ID! @@ -1079,6 +1515,48 @@ type RevokeApiKeySuccess { apiKey: ApiKey! } +type Rule { + actions: [RuleAction!]! + createdAt: Date! + enabled: Boolean! + filter: String! + id: ID! + name: String! + updatedAt: Date! +} + +type RuleAction { + params: [String!]! + type: RuleActionType! +} + +input RuleActionInput { + params: [String!]! + type: RuleActionType! +} + +enum RuleActionType { + ADD_LABEL + ARCHIVE + MARK_AS_READ + SEND_NOTIFICATION +} + +type RulesError { + errorCodes: [RulesErrorCode!]! +} + +enum RulesErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union RulesResult = RulesError | RulesSuccess + +type RulesSuccess { + rules: [Rule!]! +} + type SaveArticleReadingProgressError { errorCodes: [SaveArticleReadingProgressErrorCode!]! } @@ -1093,6 +1571,7 @@ input SaveArticleReadingProgressInput { id: ID! readingProgressAnchorIndex: Int! readingProgressPercent: Float! + readingProgressTopPercent: Float } union SaveArticleReadingProgressResult = SaveArticleReadingProgressError | SaveArticleReadingProgressSuccess @@ -1107,21 +1586,50 @@ type SaveError { } enum SaveErrorCode { + EMBEDDED_HIGHLIGHT_FAILED UNAUTHORIZED UNKNOWN } input SaveFileInput { clientRequestId: ID! + labels: [CreateLabelInput!] source: String! + state: ArticleSavingRequestStatus uploadFileId: ID! url: String! } +type SaveFilterError { + errorCodes: [SaveFilterErrorCode!]! +} + +enum SaveFilterErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input SaveFilterInput { + description: String + filter: String! + id: ID + name: String! +} + +union SaveFilterResult = SaveFilterError | SaveFilterSuccess + +type SaveFilterSuccess { + filter: Filter! +} + input SavePageInput { clientRequestId: ID! + labels: [CreateLabelInput!] originalContent: String! + parseResult: ParseResult source: String! + state: ArticleSavingRequestStatus title: String url: String! } @@ -1135,7 +1643,9 @@ type SaveSuccess { input SaveUrlInput { clientRequestId: ID! + labels: [CreateLabelInput!] source: String! + state: ArticleSavingRequestStatus url: String! } @@ -1144,12 +1654,14 @@ type SearchError { } enum SearchErrorCode { + QUERY_TOO_LONG UNAUTHORIZED } type SearchItem { annotation: String author: String + content: String contentReader: ContentReader! createdAt: Date! description: String @@ -1168,8 +1680,11 @@ type SearchItem { readAt: Date readingProgressAnchorIndex: Int! readingProgressPercent: Float! + readingProgressTopPercent: Float + recommendations: [Recommendation!] savedAt: Date! shortId: String + siteIcon: String siteName: String slug: String! state: ArticleSavingRequestStatus @@ -1180,6 +1695,7 @@ type SearchItem { updatedAt: Date uploadFileId: ID url: String! + wordsCount: Int } type SearchItemEdge { @@ -1287,8 +1803,9 @@ enum SetIntegrationErrorCode { input SetIntegrationInput { enabled: Boolean! id: ID + name: String! token: String! - type: IntegrationType! + type: IntegrationType } union SetIntegrationResult = SetIntegrationError | SetIntegrationSuccess @@ -1323,6 +1840,31 @@ type SetLabelsSuccess { labels: [Label!]! } +type SetRuleError { + errorCodes: [SetRuleErrorCode!]! +} + +enum SetRuleErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input SetRuleInput { + actions: [RuleActionInput!]! + description: String + enabled: Boolean! + filter: String! + id: ID + name: String! +} + +union SetRuleResult = SetRuleError | SetRuleSuccess + +type SetRuleSuccess { + rule: Rule! +} + type SetShareArticleError { errorCodes: [SetShareArticleErrorCode!]! } @@ -1373,6 +1915,7 @@ type SetUserPersonalizationError { } enum SetUserPersonalizationErrorCode { + NOT_FOUND UNAUTHORIZED } @@ -1382,6 +1925,10 @@ input SetUserPersonalizationInput { libraryLayoutType: String librarySortOrder: SortOrder margin: Int + speechRate: String + speechSecondaryVoice: String + speechVoice: String + speechVolume: String theme: String } @@ -1485,6 +2032,7 @@ type SubscribeSuccess { type Subscription { createdAt: Date! description: String + icon: String id: ID! name: String! newsletterEmail: String! @@ -1576,6 +2124,8 @@ enum UpdateHighlightErrorCode { input UpdateHighlightInput { annotation: String highlightId: ID! + html: String + quote: String sharedAt: Date } @@ -1664,8 +2214,11 @@ enum UpdatePageErrorCode { } input UpdatePageInput { + byline: String description: String pageId: ID! + publishedAt: Date + savedAt: Date title: String } @@ -1818,6 +2371,28 @@ enum UploadFileStatus { INITIALIZED } +type UploadImportFileError { + errorCodes: [UploadImportFileErrorCode!]! +} + +enum UploadImportFileErrorCode { + BAD_REQUEST + UNAUTHORIZED + UPLOAD_DAILY_LIMIT_EXCEEDED +} + +union UploadImportFileResult = UploadImportFileError | UploadImportFileSuccess + +type UploadImportFileSuccess { + uploadSignedUrl: String +} + +enum UploadImportFileType { + MATTER + POCKET + URL_LIST +} + type User { followersCount: Int friendsCount: Int @@ -1851,6 +2426,10 @@ type UserPersonalization { libraryLayoutType: String librarySortOrder: SortOrder margin: Int + speechRate: String + speechSecondaryVoice: String + speechVoice: String + speechVolume: String theme: String } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt index 6a354fed4..8b539b918 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/HighlightActionHandlers.kt @@ -15,10 +15,10 @@ suspend fun DataService.createWebHighlight(jsonString: String) { val highlight = Highlight( highlightId = createHighlightInput.id, shortId = createHighlightInput.shortId, - quote = createHighlightInput.quote, + quote = createHighlightInput.quote.getOrNull(), prefix = null, suffix = null, - patch = createHighlightInput.patch, + patch = createHighlightInput.patch.getOrNull(), annotation = createHighlightInput.annotation.getOrNull(), createdAt = null, updatedAt = null, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index a5f558f9f..adb4fe09c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -62,7 +62,8 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S slug = it.slug, isArchived = it.isArchived, contentReader = it.contentReader.rawValue, - content = null + content = null, + wordsCount = it.wordsCount ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt index 829c9aa66..b1c949474 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/SyncOfflineChanges.kt @@ -7,10 +7,13 @@ import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.Highlight import app.omnivore.omnivore.persistence.entities.SavedItem import com.apollographql.apollo3.api.Optional +import com.apollographql.apollo3.api.Optional.Companion.absent +import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.util.* suspend fun DataService.startSyncChannels() { for (savedItem in savedItemSyncChannel) { @@ -130,8 +133,8 @@ private suspend fun DataService.syncHighlight(highlight: Highlight) { annotation = Optional.presentIfNotNull(highlight.annotation), articleId = savedItemID ?: "", id = highlight.highlightId, - patch = highlight.patch ?: "", - quote = highlight.quote ?: "", + patch = Optional.presentIfNotNull(highlight.patch), + quote = Optional.presentIfNotNull(highlight.quote), shortId = highlight.shortId ?: "" ) ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt index 97260c72d..baf676707 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/HighlightMutations.kt @@ -24,8 +24,8 @@ data class CreateHighlightParams( annotation = Optional.presentIfNotNull(`annotation`), articleId = articleId ?: "", id = id ?: "", - patch = patch ?: "", - quote = quote ?: "", + patch = Optional.presentIfNotNull(patch), + quote = Optional.presentIfNotNull(quote), shortId = shortId ?: "" ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt index bc5ec8840..bfad2f85b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SavedItemQuery.kt @@ -77,7 +77,8 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse { slug = article.articleFields.slug, isArchived = article.articleFields.isArchived, contentReader = article.articleFields.contentReader.rawValue, - content = article.articleFields.content + content = article.articleFields.content, + wordsCount = article.articleFields.wordsCount ) return SavedItemQueryResponse(item = savedItem, highlights, labels = savedItemLabels, state = article.articleFields.state?.rawValue ?: "") diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt index 5a1811474..45bb98d1a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt @@ -86,7 +86,8 @@ suspend fun Networker.search( slug = it.node.slug, isArchived = it.node.isArchived, contentReader = it.node.contentReader.rawValue, - content = null + content = null, + wordsCount = it.node.wordsCount, ), labels = (it.node.labels ?: listOf()).map { label -> SavedItemLabel( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt index 2557e4e7e..5d8322175 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/AppDatabase.kt @@ -13,7 +13,7 @@ import app.omnivore.omnivore.persistence.entities.* SavedItemAndSavedItemLabelCrossRef::class, SavedItemAndHighlightCrossRef::class ], - version = 3 + version = 5 ) abstract class AppDatabase : RoomDatabase() { abstract fun viewerDao(): ViewerDao diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt index 757a8ee42..fa7b6a45e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/Highlight.kt @@ -11,9 +11,9 @@ data class Highlight( val createdAt: String?, val createdByMe: Boolean, val markedForDeletion: Boolean = false, - var patch: String, + var patch: String?, var prefix: String?, - var quote: String, + var quote: String?, var serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue, var shortId: String, val suffix: String?, diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index ae493ebc2..9ad6c5cff 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -40,7 +40,8 @@ data class SavedItem( val originalHtml: String? = null, @ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null, var serverSyncStatus: Int = 0, - val tempPDFURL: String? = null + val tempPDFURL: String? = null, + val wordsCount: Int? = null // hasMany highlights // hasMany labels @@ -78,6 +79,7 @@ data class SavedItemCardData( val contentReader: String?, val savedAt: String, val readingProgress: Double, + val wordsCount: Int? ) { fun publisherDisplayName(): String? { return publisherURLString?.toUri()?.host @@ -161,5 +163,5 @@ interface SavedItemDao { object SavedItemQueryConstants { - const val columns = "savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress" + const val columns = "savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress, wordsCount" } 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 a1c45a9ba..2e6812113 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 @@ -14,6 +14,7 @@ import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.* +import com.apollographql.apollo3.api.Optional import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel @@ -141,7 +142,8 @@ class LibraryViewModel @Inject constructor( imageURLString = it.imageURLString, isArchived = it.isArchived, pageURLString = it.pageURLString, - contentReader = it.contentReader + contentReader = it.contentReader, + wordsCount = it.wordsCount ), labels = listOf() ) @@ -222,7 +224,8 @@ class LibraryViewModel @Inject constructor( pageURLString = it.pageURLString, contentReader = it.contentReader, savedAt = it.savedAt, - readingProgress = it.readingProgress + readingProgress = it.readingProgress, + wordsCount = it.wordsCount ), labels = listOf() ) @@ -388,7 +391,7 @@ class LibraryViewModel @Inject constructor( fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { viewModelScope.launch { withContext(Dispatchers.IO) { - val newLabel = networker.createNewLabel(CreateLabelInput(color = hexColorValue, name = labelName)) + val newLabel = networker.createNewLabel(CreateLabelInput(color = Optional.presentIfNotNull(hexColorValue), name = labelName)) newLabel?.let { val savedItemLabel = SavedItemLabel( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt index d6249cd21..3cbd729d2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReader.kt @@ -134,13 +134,16 @@ class PDFReaderActivity: AppCompatActivity(), DocumentListener, TextSelectionMan private fun loadHighlights(highlights: List) { for (highlight in highlights) { - val highlightAnnotation = fragment - .document - ?.annotationProvider - ?.createAnnotationFromInstantJson(highlight.patch) + val patch = highlight.patch + if (patch != null) { + val highlightAnnotation = fragment + .document + ?.annotationProvider + ?.createAnnotationFromInstantJson(patch) - highlightAnnotation?.let { - fragment.addAnnotationToPage(highlightAnnotation, true) + highlightAnnotation?.let { + fragment.addAnnotationToPage(highlightAnnotation, true) + } } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt index f5d37fc62..d5ec05cd7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/PDFReaderViewModel.kt @@ -133,8 +133,8 @@ class PDFReaderViewModel @Inject constructor( annotation = Optional.presentIfNotNull(note), articleId = itemID, id = highlightID, - patch = newAnnotation.toInstantJson(), - quote = quote, + patch = Optional.presentIfNotNull(newAnnotation.toInstantJson()), + quote = Optional.presentIfNotNull(quote), shortId = shortID, ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt index ad442fbf9..38d037106 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/reader/WebReaderViewModel.kt @@ -19,6 +19,8 @@ import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.library.SavedItemAction +import com.apollographql.apollo3.api.Optional +import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* @@ -394,7 +396,8 @@ class WebReaderViewModel @Inject constructor( fun createNewSavedItemLabel(labelName: String, hexColorValue: String) { viewModelScope.launch { withContext(Dispatchers.IO) { - val newLabel = networker.createNewLabel(CreateLabelInput(color = hexColorValue, name = labelName)) + + val newLabel = networker.createNewLabel(CreateLabelInput(color = Optional.presentIfNotNull(hexColorValue), name = labelName)) newLabel?.let { val savedItemLabel = SavedItemLabel( diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index ff85bfa7b..7ce81ce76 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -53,6 +53,8 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC .padding(end = 20.dp) .defaultMinSize(minHeight = 55.dp) ) { + readInfo(item = cardData) + Text( text = cardData.title, style = TextStyle( @@ -66,7 +68,11 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC if (cardData.author != null && cardData.author != "") { Text( text = byline(cardData), - style = MaterialTheme.typography.bodyMedium, + style = TextStyle( + fontSize = 15.sp, + fontWeight = FontWeight.Normal, + color = Color(red = 137, green = 137, blue = 137) + ), maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -88,7 +94,7 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC contentDescription = "Image associated with saved item", modifier = Modifier .size(55.dp, 73.dp) - .clip(RoundedCornerShape(4.dp)) + .clip(RoundedCornerShape(10.dp)) ) } } @@ -141,3 +147,96 @@ fun byline(item: SavedItemCardData): String { return "" } + +// +//var readingSpeed: Int64 { +// var result = UserDefaults.standard.integer(forKey: UserDefaultKey.userWordsPerMinute.rawValue) +// if result <= 0 { +// result = 235 +// } +// return Int64(result) +//} + +fun estimatedReadingTime(item: SavedItemCardData): String { + item.wordsCount?.let { + if (it > 0) { + val readLen = Math.max(1, it / 235) + return "$readLen MIN READ • " + } + } + return "" +} + +fun readingProgress(item: SavedItemCardData): String { + // If there is no wordsCount don't show progress because it will make no sense + item.wordsCount?.let { + if (it > 0) { + val intVal = item.readingProgress.toInt() + return "$intVal%" + } + } + return "" +} +// +//var highlightsText: String { +// if let highlights = item.highlights, highlights.count > 0 { +// let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count) +// if item.wordsCount > 0 { +// return " • \(fmted)" +// } +// return fmted +// } +// return "" +//} +// +//var notesText: String { +// let notes = item.highlights?.filter { item in +// if let highlight = item as? Highlight { +// return !(highlight.annotation ?? "").isEmpty +// } +// return false +// } +// +// if let notes = notes, notes.count > 0 { +// let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count) +// if item.wordsCount > 0 { +// return " • \(fmted)" +// } +// return fmted +// } +// return "" +//} + +@Composable +fun readInfo(item: SavedItemCardData) { + Row( + modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = 15.dp) + ) { + Text( + text = estimatedReadingTime(item), + style = TextStyle( + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + color = Color(red = 137, green = 137, blue = 137) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = readingProgress(item), + style = TextStyle( + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + color = if (item.readingProgress > 1) Color(red = 85, green = 185, blue = 56) else Color(red = 137, green = 137, blue = 137) + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + +// Text("\(highlightsText)") +// +// Text("\(notesText)") + + } +} From 7e1eefb4e76528ca1b16d265ea4a8be140117180 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 10:56:49 +0800 Subject: [PATCH 11/24] Add colours to resources --- .../omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt | 8 ++++++-- android/Omnivore/app/src/main/res/values/colors.xml | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index 7ce81ce76..4178b70ce 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -13,10 +13,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* +import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.components.LabelChipColors @@ -210,7 +212,9 @@ fun readingProgress(item: SavedItemCardData): String { @Composable fun readInfo(item: SavedItemCardData) { Row( - modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = 15.dp) + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 15.dp) ) { Text( text = estimatedReadingTime(item), @@ -228,7 +232,7 @@ fun readInfo(item: SavedItemCardData) { style = TextStyle( fontSize = 11.sp, fontWeight = FontWeight.Medium, - color = if (item.readingProgress > 1) Color(red = 85, green = 185, blue = 56) else Color(red = 137, green = 137, blue = 137) + color = if (item.readingProgress > 1) colorResource(R.color.green_55B938) else colorResource(R.color.gray_898989) ), maxLines = 1, overflow = TextOverflow.Ellipsis diff --git a/android/Omnivore/app/src/main/res/values/colors.xml b/android/Omnivore/app/src/main/res/values/colors.xml index ee4d3d2e0..a7080a1fd 100644 --- a/android/Omnivore/app/src/main/res/values/colors.xml +++ b/android/Omnivore/app/src/main/res/values/colors.xml @@ -4,4 +4,7 @@ #FFFFFFFF #F7F7F7 #B7B7B7 + + #898989 + #55B938 From 7070819881bb257bf4f7861af1ca5202094ea706 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 12:00:01 +0800 Subject: [PATCH 12/24] Add a library filter button to clear current search --- .../omnivore/ui/library/LibraryFilterBar.kt | 40 ++++++++++++++++++ .../omnivore/ui/library/LibraryViewModel.kt | 41 ------------------- .../src/main/res/drawable/funnel_simple.xml | 9 ++++ 3 files changed, 49 insertions(+), 41 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/drawable/funnel_simple.xml 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 1e3dad14a..a9febf7e3 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 @@ -1,18 +1,24 @@ package app.omnivore.omnivore.ui.library +import androidx.compose.ui.graphics.Color import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.components.LabelChipColors @@ -23,6 +29,7 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) { val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX) val activeLabels: List by viewModel.activeLabelsLiveData.observeAsState(listOf()) + val searchText: String by viewModel.searchTextLiveData.observeAsState("") var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) } val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(SavedItemSortFilter.NEWEST) val listState = rememberLazyListState() @@ -37,6 +44,39 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) { .fillMaxWidth() ) { item { + if (searchText.isNotEmpty()) { + AssistChip( + onClick = { + viewModel.updateSearchText("") + }, + label = { Text(searchText) }, + border = null, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + containerColor = colorResource(R.color.gray_898989), + labelColor = Color.White, + iconContentColor = Color.White + ), + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.funnel_simple), + contentDescription = "Clear current search", + modifier = Modifier.size(17.dp), + tint = Color.White + ) + }, + trailingIcon = { + Icon( + Icons.Default.Clear, + contentDescription = "close icon to remove label", + modifier = Modifier.size(15.dp), + tint = Color.White + ) + + }, + modifier = Modifier + .padding(horizontal = 4.dp) + ) + } AssistChip( onClick = { isSavedItemFilterMenuExpanded = true }, label = { Text(activeSavedItemFilter.displayText) }, 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 2e6812113..13e5e2b4b 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 @@ -110,47 +110,6 @@ class LibraryViewModel @Inject constructor( viewModelScope.launch { itemsLiveData.postValue(listOf()) librarySearchCursor = null - - 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.slug) - - if (!isSavedInDB) { - delay(2000) - contentRequestChannel.send(it.slug) - } - } - - val newItems = result.savedItems.map { - SavedItemCardDataWithLabels( - cardData = SavedItemCardData( - savedItemId = it.savedItemId, - savedAt = it.savedAt, - readingProgress = it.readingProgress, - slug = it.slug, - publisherURLString = it.publisherURLString, - title = it.title, - author = it.author, - imageURLString = it.imageURLString, - isArchived = it.isArchived, - pageURLString = it.pageURLString, - contentReader = it.contentReader, - wordsCount = it.wordsCount - ), - labels = listOf() - ) - } - - itemsLiveData.postValue(newItems) - } } } diff --git a/android/Omnivore/app/src/main/res/drawable/funnel_simple.xml b/android/Omnivore/app/src/main/res/drawable/funnel_simple.xml new file mode 100644 index 000000000..a85f9ec01 --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/funnel_simple.xml @@ -0,0 +1,9 @@ + + + From 7fbd9c59211059759924f5057918c30467675e38 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 13:12:47 +0800 Subject: [PATCH 13/24] Use a standalone searchview and model to better match Android best practices --- .../app/omnivore/omnivore/MainActivity.kt | 4 +- .../omnivore/ui/library/SearchView.kt | 106 ++++++--- .../omnivore/ui/library/SearchViewModel.kt | 206 ++++++++++++++++++ .../app/omnivore/omnivore/ui/root/RootView.kt | 8 +- 4 files changed, 293 insertions(+), 31 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index b37795e62..64c58df6c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -15,6 +15,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.library.LibraryViewModel +import app.omnivore.omnivore.ui.library.SearchViewModel import app.omnivore.omnivore.ui.root.RootView import app.omnivore.omnivore.ui.settings.SettingsViewModel import app.omnivore.omnivore.ui.theme.OmnivoreTheme @@ -33,6 +34,7 @@ class MainActivity : ComponentActivity() { val loginViewModel: LoginViewModel by viewModels() val libraryViewModel: LibraryViewModel by viewModels() val settingsViewModel: SettingsViewModel by viewModels() + val searchViewModel: SearchViewModel by viewModels() val context = this @@ -53,7 +55,7 @@ class MainActivity : ComponentActivity() { .fillMaxSize() .background(color = Color.Black) ) { - RootView(loginViewModel, libraryViewModel, settingsViewModel) + RootView(loginViewModel, searchViewModel, libraryViewModel, settingsViewModel) } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt index 264f6017b..33386069c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -1,47 +1,39 @@ package app.omnivore.omnivore.ui.library import android.content.Intent -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.* 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.material.ExperimentalMaterialApi -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import app.omnivore.omnivore.Routes -import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels -import app.omnivore.omnivore.ui.components.LabelsSelectionSheet -import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard -import app.omnivore.omnivore.ui.reader.PDFReaderActivity -import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity -import kotlinx.coroutines.flow.distinctUntilChanged -import androidx.compose.ui.res.stringResource import app.omnivore.omnivore.R +import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels +import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity import app.omnivore.omnivore.persistence.entities.TypeaheadCardData +import app.omnivore.omnivore.ui.reader.PDFReaderActivity +import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.savedItemViews.TypeaheadSearchCard @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchView( - libraryViewModel: LibraryViewModel, + viewModel: SearchViewModel, navController: NavHostController ) { - val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("") + val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false) + val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true) + val searchText: String by viewModel.searchTextLiveData.observeAsState("") Scaffold( topBar = { @@ -52,10 +44,9 @@ fun SearchView( SearchField( searchText, onSearch = { - libraryViewModel.performSearch() - navController.popBackStack() + viewModel.performSearch() }, - onSearchTextChanged = { libraryViewModel.updateSearchText(it) }, + onSearchTextChanged = { viewModel.updateSearchText(it) }, navController = navController ) } @@ -64,20 +55,44 @@ fun SearchView( } ) { paddingValues -> - SearchViewContent( - libraryViewModel, - modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()) - ) + if (isRefreshing) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .padding(top = paddingValues.calculateTopPadding()) + .fillMaxWidth() + ) { + CircularProgressIndicator( + modifier = Modifier + .height(45.dp) + .width(45.dp), + strokeWidth = 5.dp, + color = colorResource(R.color.green_55B938) + ) + } + } else if (typeaheadMode) { + TypeaheadSearchViewContent( + viewModel, + modifier = Modifier + .padding(top = paddingValues.calculateTopPadding()) + ) + } else { + SearchViewContent( + viewModel, + modifier = Modifier + .padding(top = paddingValues.calculateTopPadding()) + ) + } } } @OptIn(ExperimentalMaterialApi::class) @Composable -fun SearchViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { +fun TypeaheadSearchViewContent(viewModel: SearchViewModel, modifier: Modifier) { val context = LocalContext.current val listState = rememberLazyListState() - val searchedCardsData: List by libraryViewModel.searchItemsLiveData.observeAsState(listOf()) + + val searchedCardsData: List by viewModel.searchItemsLiveData.observeAsState(listOf()) LazyColumn( state = listState, @@ -98,8 +113,41 @@ fun SearchViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { intent.putExtra("SAVED_ITEM_SLUG", cardData.slug) context.startActivity(intent) }, - actionHandler = { libraryViewModel.handleSavedItemAction(cardData.savedItemId, it) } + actionHandler = { viewModel.handleSavedItemAction(cardData.savedItemId, it) } ) } } } + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SearchViewContent(viewModel: SearchViewModel, modifier: Modifier) { + val context = LocalContext.current + val listState = rememberLazyListState() + + val cardsData: List by viewModel.itemsLiveData.observeAsState(listOf()) + + LazyColumn( + state = listState, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + + ) { + items(cardsData) { cardDataWithLabels -> + SavedItemCard( + cardData = cardDataWithLabels.cardData, + labels = cardDataWithLabels.labels, + onClickHandler = { + val activityClass = if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + val intent = Intent(context, activityClass) + intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug) + context.startActivity(intent) + }, + actionHandler = { viewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) } + ) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt new file mode 100644 index 000000000..1484bce27 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt @@ -0,0 +1,206 @@ +package app.omnivore.omnivore.ui.library + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.* +import app.omnivore.omnivore.dataService.* +import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput +import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput +import app.omnivore.omnivore.networking.* +import app.omnivore.omnivore.persistence.entities.* +import com.apollographql.apollo3.api.Optional +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.time.Instant +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val networker: Networker, + private val dataService: DataService, + private val datastoreRepo: DatastoreRepository +): ViewModel() { + private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) + + 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 + + // Live Data + var isRefreshing = MutableLiveData(false) + val typeaheadMode = MutableLiveData(true) + val searchTextLiveData = MutableLiveData("") + val searchItemsLiveData = MutableLiveData>(listOf()) + val itemsLiveData = MediatorLiveData>() + + fun updateSearchText(text: String) { + typeaheadMode.postValue(true) + searchTextLiveData.value = text + + if (text == "") { + searchItemsLiveData.value = listOf() + } else { + viewModelScope.launch { + performTypeaheadSearch(true) + } + } + } + + fun performSearch() { + // To perform search we just clear the current state, so the LibraryView infinite scroll + // load will update items. + viewModelScope.launch { + isRefreshing.postValue(true) + itemsLiveData.postValue(listOf()) + typeaheadMode.postValue(false) + + loadUsingSearchAPI() + } + } + + fun loadUsingSearchAPI() { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val result = dataService.librarySearch(cursor = librarySearchCursor, query = searchQueryString()) + result.cursor?.let { + librarySearchCursor = it + } + + result.savedItems.map { + val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.slug) + + if (!isSavedInDB) { + delay(2000) + contentRequestChannel.send(it.slug) + } + } + + val newItems = result.savedItems.map { + SavedItemCardDataWithLabels( + cardData = SavedItemCardData( + savedItemId = it.savedItemId, + slug = it.slug, + publisherURLString = it.publisherURLString, + title = it.title, + author = it.author, + imageURLString = it.imageURLString, + isArchived = it.isArchived, + pageURLString = it.pageURLString, + contentReader = it.contentReader, + savedAt = it.savedAt, + readingProgress = it.readingProgress, + wordsCount = it.wordsCount + ), + labels = listOf() + ) + } + + itemsLiveData.value?.let{ + itemsLiveData.postValue(newItems + it) + } ?: run { + itemsLiveData.postValue(newItems) + } + + isRefreshing.postValue(false) + } + } + } + + private suspend fun performTypeaheadSearch(clearPreviousSearch: Boolean) { + if (clearPreviousSearch) { + cursor = null + } + + val thisSearchIdx = searchIdx + searchIdx += 1 + + // Execute the search + val searchResult = networker.typeaheadSearch(searchTextLiveData.value ?: "") + + // Search results aren't guaranteed to return in order so this + // will discard old results that are returned while a user is typing. + // For example if a user types 'Canucks', often the search results + // for 'C' are returned after 'Canucks' because it takes the backend + // much longer to compute. + if (thisSearchIdx in 1..receivedIdx) { + return + } + + searchItemsLiveData.postValue(searchResult.cardsData) + + isRefreshing.postValue(false) + } + + fun handleSavedItemAction(itemID: String, action: SavedItemAction) { +// when (action) { +// SavedItemAction.Delete -> { +// viewModelScope.launch { +// dataService.deleteSavedItem(itemID) +// } +// } +// SavedItemAction.Archive -> { +// viewModelScope.launch { +// dataService.archiveSavedItem(itemID) +// } +// } +// SavedItemAction.Unarchive -> { +// viewModelScope.launch { +// dataService.unarchiveSavedItem(itemID) +// } +// } +// SavedItemAction.EditLabels -> { +// labelsSelectionCurrentItemLiveData.value = itemID +// showLabelsSelectionSheetLiveData.value = true +// } +// } + } + + fun updateSavedItemLabels(savedItemID: String, labels: List) { +// viewModelScope.launch { +// withContext(Dispatchers.IO) { +// val input = SetLabelsInput(labelIds = labels.map { it.savedItemLabelId }, pageId = savedItemID) +// val networkResult = networker.updateLabelsForSavedItem(input) +// +// // TODO: assign a server sync status to these +// val crossRefs = labels.map { +// SavedItemAndSavedItemLabelCrossRef( +// savedItemLabelId = it.savedItemLabelId, +// savedItemId = savedItemID +// ) +// } +// +// // Remove all labels first +// dataService.db.savedItemAndSavedItemLabelCrossRefDao().deleteRefsBySavedItemId(savedItemID) +// +// // Add back the current labels +// dataService.db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) +// +// CoroutineScope(Dispatchers.Main).launch { +// handleFilterChanges() +// } +// } +// } + } + + private fun searchQueryString(): String { + var query = "" + val searchText = searchTextLiveData.value ?: "" + + if (searchText.isNotEmpty()) { + query += " $searchText" + } + + return query + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt index 64b81ef36..c9b7e0c10 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt @@ -13,12 +13,15 @@ import androidx.compose.ui.graphics.Color import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.Routes +import app.omnivore.omnivore.dataService.DataService import app.omnivore.omnivore.ui.auth.LoginViewModel import app.omnivore.omnivore.ui.auth.WelcomeScreen import app.omnivore.omnivore.ui.library.LibraryView import app.omnivore.omnivore.ui.library.SearchView import app.omnivore.omnivore.ui.library.LibraryViewModel +import app.omnivore.omnivore.ui.library.SearchViewModel import app.omnivore.omnivore.ui.settings.PolicyWebView import app.omnivore.omnivore.ui.settings.SettingsViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -26,6 +29,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController @Composable fun RootView( loginViewModel: LoginViewModel, + searchViewModel: SearchViewModel, libraryViewModel: LibraryViewModel, settingsViewModel: SettingsViewModel ) { @@ -49,6 +53,7 @@ fun RootView( if (hasAuthToken) { PrimaryNavigator( loginViewModel = loginViewModel, + searchViewModel = searchViewModel, libraryViewModel = libraryViewModel, settingsViewModel = settingsViewModel ) @@ -69,6 +74,7 @@ fun RootView( fun PrimaryNavigator( loginViewModel: LoginViewModel, libraryViewModel: LibraryViewModel, + searchViewModel: SearchViewModel, settingsViewModel: SettingsViewModel ) { val navController = rememberNavController() @@ -83,7 +89,7 @@ fun PrimaryNavigator( composable(Routes.Search.route) { SearchView( - libraryViewModel = libraryViewModel, + viewModel = searchViewModel, navController = navController ) } From 928326014b4e521515104699e7423594efd56d81 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 13:18:26 +0800 Subject: [PATCH 14/24] Remove search text from LibraryViewModel as this is all done in SearchViewModel now --- .../omnivore/ui/library/LibraryFilterBar.kt | 34 ------------ .../omnivore/ui/library/LibraryView.kt | 3 +- .../omnivore/ui/library/LibraryViewModel.kt | 55 ------------------- .../omnivore/omnivore/ui/library/SearchBar.kt | 2 - 4 files changed, 1 insertion(+), 93 deletions(-) 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 a9febf7e3..f7a6c5660 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 @@ -29,7 +29,6 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) { val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX) val activeLabels: List by viewModel.activeLabelsLiveData.observeAsState(listOf()) - val searchText: String by viewModel.searchTextLiveData.observeAsState("") var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) } val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(SavedItemSortFilter.NEWEST) val listState = rememberLazyListState() @@ -44,39 +43,6 @@ fun LibraryFilterBar(viewModel: LibraryViewModel) { .fillMaxWidth() ) { item { - if (searchText.isNotEmpty()) { - AssistChip( - onClick = { - viewModel.updateSearchText("") - }, - label = { Text(searchText) }, - border = null, - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - containerColor = colorResource(R.color.gray_898989), - labelColor = Color.White, - iconContentColor = Color.White - ), - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.funnel_simple), - contentDescription = "Clear current search", - modifier = Modifier.size(17.dp), - tint = Color.White - ) - }, - trailingIcon = { - Icon( - Icons.Default.Clear, - contentDescription = "close icon to remove label", - modifier = Modifier.size(15.dp), - tint = Color.White - ) - - }, - modifier = Modifier - .padding(horizontal = 4.dp) - ) - } AssistChip( onClick = { isSavedItemFilterMenuExpanded = true }, label = { Text(activeSavedItemFilter.displayText) }, 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 3942f4eae..20fb83f8a 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 @@ -100,8 +100,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { } InfiniteListHandler(listState = listState) { - val searchText = libraryViewModel.searchTextLiveData.value ?: "" - if (cardsData.isEmpty() && searchText.isEmpty()) { + if (cardsData.isEmpty()) { Log.d("sync", "loading with load func") libraryViewModel.initialLoad() } else { 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 13e5e2b4b..54eb01c84 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 @@ -38,8 +38,6 @@ class LibraryViewModel @Inject constructor( private var receivedIdx = 0 // Live Data - val searchTextLiveData = MutableLiveData("") - val searchItemsLiveData = MutableLiveData>(listOf()) private var itemsLiveDataInternal = dataService.libraryLiveData(SavedItemFilter.INBOX, SavedItemSortFilter.NEWEST, listOf()) val itemsLiveData = MediatorLiveData>() val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) @@ -92,27 +90,6 @@ class LibraryViewModel @Inject constructor( } } - fun updateSearchText(text: String) { - searchTextLiveData.value = text - - if (text == "") { - searchItemsLiveData.value = listOf() - } else { - viewModelScope.launch { - performTypeaheadSearch(true) - } - } - } - - fun performSearch() { - // To perform search we just clear the current state, so the LibraryView infinite scroll - // load will update items. - viewModelScope.launch { - itemsLiveData.postValue(listOf()) - librarySearchCursor = null - } - } - fun refresh() { isRefreshing = true load(true) @@ -269,33 +246,6 @@ class LibraryViewModel @Inject constructor( } } - private suspend fun performTypeaheadSearch(clearPreviousSearch: Boolean) { - if (clearPreviousSearch) { - cursor = null - } - - val thisSearchIdx = searchIdx - searchIdx += 1 - - // Execute the search - val searchResult = networker.typeaheadSearch(searchTextLiveData.value ?: "") - - // Search results aren't guaranteed to return in order so this - // will discard old results that are returned while a user is typing. - // For example if a user types 'Canucks', often the search results - // for 'C' are returned after 'Canucks' because it takes the backend - // much longer to compute. - if (thisSearchIdx in 1..receivedIdx) { - return - } - - searchItemsLiveData.postValue(searchResult.cardsData) - - CoroutineScope(Dispatchers.Main).launch { - isRefreshing = false - } - } - fun handleSavedItemAction(itemID: String, action: SavedItemAction) { when (action) { SavedItemAction.Delete -> { @@ -377,11 +327,6 @@ class LibraryViewModel @Inject constructor( private fun searchQueryString(): String { var query = "${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}" - val searchText = searchTextLiveData.value ?: "" - - if (searchText.isNotEmpty()) { - query += " $searchText" - } activeLabelsLiveData.value?.let { if (it.isNotEmpty()) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt index cb3d0a149..6924b7fbe 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt @@ -34,8 +34,6 @@ fun SearchBar( onSearchClicked: () -> Unit, onSettingsIconClick: () -> Unit ) { - val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("") - TopAppBar( title = { Text("Library") From 326905a08da012ca73746d9ae9b7cc51a5fab80c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 14:31:42 +0800 Subject: [PATCH 15/24] Rename SearchBar to LibraryNavBar, use person icon instead of settings --- .../{SearchBar.kt => LibraryNavigationBar.kt} | 34 +++++++------------ .../omnivore/ui/library/LibraryView.kt | 4 +-- 2 files changed, 15 insertions(+), 23 deletions(-) rename android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/{SearchBar.kt => LibraryNavigationBar.kt} (76%) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt similarity index 76% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt index 6924b7fbe..7f8c0b24e 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt @@ -3,34 +3,24 @@ package app.omnivore.omnivore.ui.library import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import app.omnivore.omnivore.R @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SearchBar( - libraryViewModel: LibraryViewModel, +fun LibraryNavigationBar( onSearchClicked: () -> Unit, onSettingsIconClick: () -> Unit ) { @@ -38,9 +28,11 @@ fun SearchBar( title = { Text("Library") }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ), + modifier = Modifier.statusBarsPadding(), +// colors = TopAppBarDefaults.topAppBarColors( +// // containerColor = MaterialTheme.colorScheme.background, +// // scrolledContainerColor = colorResource(R.color.gray_B7B7B7) +// ), actions = { IconButton(onClick = onSearchClicked) { Icon( @@ -51,7 +43,7 @@ fun SearchBar( IconButton(onClick = onSettingsIconClick) { Icon( - imageVector = Icons.Default.Settings, + imageVector = Icons.Default.Person, contentDescription = null ) } @@ -73,11 +65,11 @@ fun SearchField( TextField( modifier = Modifier - .fillMaxWidth() - .onFocusChanged { focusState -> - showClearButton = (focusState.isFocused) - } - .focusRequester(focusRequester), + .fillMaxWidth() + .onFocusChanged { focusState -> + showClearButton = (focusState.isFocused) + } + .focusRequester(focusRequester), value = searchText, onValueChange = onSearchTextChanged, placeholder = { 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 20fb83f8a..07f5ada35 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 @@ -37,11 +37,11 @@ fun LibraryView( ) { Scaffold( topBar = { - SearchBar( - libraryViewModel = libraryViewModel, + LibraryNavigationBar( onSearchClicked = { navController.navigate(Routes.Search.route) }, onSettingsIconClick = { navController.navigate(Routes.Settings.route) } ) + } ) { paddingValues -> LibraryViewContent( From aaa2f3755b1555a29f409c03e1dbc295c553a058 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 14:38:45 +0800 Subject: [PATCH 16/24] Use the three dots icon for the menu --- .../app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt index 7f8c0b24e..5c2e01756 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt @@ -43,7 +43,7 @@ fun LibraryNavigationBar( IconButton(onClick = onSettingsIconClick) { Icon( - imageVector = Icons.Default.Person, + imageVector = Icons.Default.MoreVert, contentDescription = null ) } From 9f99b78f1ece536da509eb2719e1464c800093eb Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 15:56:12 +0800 Subject: [PATCH 17/24] Use Navigation Actions for the library item actions --- .../ui/library/LibraryNavigationBar.kt | 85 +++++++++++++++---- .../omnivore/ui/library/LibraryView.kt | 2 + .../omnivore/ui/library/LibraryViewModel.kt | 4 +- .../omnivore/ui/library/SavedItemViewModel.kt | 11 +++ .../omnivore/ui/library/SearchView.kt | 70 ++++++++++++--- .../omnivore/ui/library/SearchViewModel.kt | 4 +- .../ui/savedItemViews/SavedItemCard.kt | 6 +- .../app/src/main/res/values/colors.xml | 3 + 8 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt index 5c2e01756..c26e868a1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt @@ -3,50 +3,101 @@ package app.omnivore.omnivore.ui.library import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction +import androidx.lifecycle.MutableLiveData import androidx.navigation.NavHostController +import app.omnivore.omnivore.R +import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels @OptIn(ExperimentalMaterial3Api::class) @Composable fun LibraryNavigationBar( + savedItemViewModel: SavedItemViewModel, onSearchClicked: () -> Unit, onSettingsIconClick: () -> Unit ) { + val actionsMenuItem: SavedItemCardData? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) + TopAppBar( title = { - Text("Library") + Text(if (actionsMenuItem == null) "Library" else "") }, modifier = Modifier.statusBarsPadding(), -// colors = TopAppBarDefaults.topAppBarColors( -// // containerColor = MaterialTheme.colorScheme.background, -// // scrolledContainerColor = colorResource(R.color.gray_B7B7B7) -// ), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = if (actionsMenuItem == null) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.surfaceVariant + ), + navigationIcon = { + if (actionsMenuItem != null) { + IconButton(onClick = { + savedItemViewModel.actionsMenuItemLiveData.postValue(null) + }) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, + modifier = Modifier, + contentDescription = "Back" + ) + } + } + }, actions = { - IconButton(onClick = onSearchClicked) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = null - ) - } + if (actionsMenuItem != null) { + IconButton(onClick = onSearchClicked) { + Icon( + painter = painterResource(id = R.drawable.archive_outline), + contentDescription = null + ) + } + IconButton(onClick = onSearchClicked) { + Icon( + painter = painterResource(id = R.drawable.tag), + contentDescription = null + ) + } + IconButton(onClick = onSearchClicked) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null + ) + } + IconButton(onClick = onSettingsIconClick) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } + } else { + IconButton(onClick = onSearchClicked) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null + ) + } - IconButton(onClick = onSettingsIconClick) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null - ) - } + IconButton(onClick = onSettingsIconClick) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } + } } ) } 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 07f5ada35..516883d48 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 @@ -38,6 +38,7 @@ fun LibraryView( Scaffold( topBar = { LibraryNavigationBar( + savedItemViewModel = libraryViewModel, onSearchClicked = { navController.navigate(Routes.Search.route) }, onSettingsIconClick = { navController.navigate(Routes.Settings.route) } ) @@ -86,6 +87,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { } items(cardsData) { cardDataWithLabels -> SavedItemCard( + savedItemViewModel = libraryViewModel, cardData = cardDataWithLabels.cardData, labels = cardDataWithLabels.labels, onClickHandler = { 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 54eb01c84..6dd2f60da 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 @@ -26,7 +26,7 @@ class LibraryViewModel @Inject constructor( private val networker: Networker, private val dataService: DataService, private val datastoreRepo: DatastoreRepository -): ViewModel() { +): ViewModel(), SavedItemViewModel { private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) private var cursor: String? = null @@ -47,6 +47,8 @@ class LibraryViewModel @Inject constructor( val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() val activeLabelsLiveData = MutableLiveData>(listOf()) + override val actionsMenuItemLiveData = MutableLiveData(null) + var isRefreshing by mutableStateOf(false) var hasLoadedInitialFilters = false diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt new file mode 100644 index 000000000..fc0f9917c --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt @@ -0,0 +1,11 @@ +package app.omnivore.omnivore.ui.library + +import androidx.lifecycle.MutableLiveData +import app.omnivore.omnivore.persistence.entities.SavedItemCardData + +interface SavedItemViewModel { + + val actionsMenuItemLiveData: MutableLiveData + get() = MutableLiveData(null) + +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt index 33386069c..7a95a492f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -7,6 +7,10 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState @@ -14,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import app.omnivore.omnivore.R @@ -34,22 +39,66 @@ fun SearchView( val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false) val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true) val searchText: String by viewModel.searchTextLiveData.observeAsState("") + val actionsMenuItem: SavedItemCardData? by viewModel.actionsMenuItemLiveData.observeAsState(null) Scaffold( topBar = { TopAppBar( title = { Text("") }, - actions = { - Row { - SearchField( - searchText, - onSearch = { - viewModel.performSearch() - }, - onSearchTextChanged = { viewModel.updateSearchText(it) }, - navController = navController - ) + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + navigationIcon = { + if (actionsMenuItem != null) { + IconButton(onClick = { + viewModel.actionsMenuItemLiveData.postValue(null) + }) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, + modifier = Modifier, + contentDescription = "Back" + ) + } } + }, + actions = { + if (actionsMenuItem != null) { + IconButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.archive_outline), + contentDescription = null + ) + } + IconButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.tag), + contentDescription = null + ) + } + IconButton(onClick = { }) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null + ) + } + IconButton(onClick = { }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } + } else { + Row { + SearchField( + searchText, + onSearch = { + viewModel.performSearch() + }, + onSearchTextChanged = { viewModel.updateSearchText(it) }, + navController = navController + ) + } + } } ) @@ -138,6 +187,7 @@ fun SearchViewContent(viewModel: SearchViewModel, modifier: Modifier) { ) { items(cardsData) { cardDataWithLabels -> SavedItemCard( + savedItemViewModel = viewModel, cardData = cardDataWithLabels.cardData, labels = cardDataWithLabels.labels, onClickHandler = { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt index 1484bce27..bbaf0444d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt @@ -26,7 +26,7 @@ class SearchViewModel @Inject constructor( private val networker: Networker, private val dataService: DataService, private val datastoreRepo: DatastoreRepository -): ViewModel() { +): ViewModel(), SavedItemViewModel { private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) private var cursor: String? = null @@ -44,6 +44,8 @@ class SearchViewModel @Inject constructor( val searchItemsLiveData = MutableLiveData>(listOf()) val itemsLiveData = MediatorLiveData>() + override val actionsMenuItemLiveData = MutableLiveData(null) + fun updateSearchText(text: String) { typeaheadMode.postValue(true) searchTextLiveData.value = text diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index 4178b70ce..f5b7cc8bb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -22,13 +22,15 @@ import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemLabel import app.omnivore.omnivore.ui.components.LabelChipColors +import app.omnivore.omnivore.ui.library.LibraryViewModel import app.omnivore.omnivore.ui.library.SavedItemAction +import app.omnivore.omnivore.ui.library.SavedItemViewModel import coil.compose.rememberAsyncImagePainter @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ) @Composable -fun SavedItemCard(cardData: SavedItemCardData, labels: List, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { +fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCardData, labels: List, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { var isMenuExpanded by remember { mutableStateOf(false) } val publisherDisplayName = cardData.publisherDisplayName() val listState = rememberLazyListState() @@ -37,7 +39,7 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List, onC modifier = Modifier .combinedClickable( onClick = onClickHandler, - onLongClick = { isMenuExpanded = true } + onLongClick = { savedItemViewModel.actionsMenuItemLiveData.postValue(cardData) } ) ) { Row( diff --git a/android/Omnivore/app/src/main/res/values/colors.xml b/android/Omnivore/app/src/main/res/values/colors.xml index a7080a1fd..d81888f7f 100644 --- a/android/Omnivore/app/src/main/res/values/colors.xml +++ b/android/Omnivore/app/src/main/res/values/colors.xml @@ -7,4 +7,7 @@ #898989 #55B938 + #EBEBEB + + #E6E4BF From 4b87f1dd20d1eeaa33f3e1d6cc01400ecd8f8195 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 20 Apr 2023 17:58:26 +0800 Subject: [PATCH 18/24] Implement article actions on the library --- .../ui/library/LibraryNavigationBar.kt | 175 ++++++++++++++---- .../omnivore/ui/library/LibraryView.kt | 51 +++-- .../omnivore/ui/library/LibraryViewModel.kt | 3 +- .../omnivore/ui/library/SavedItemViewModel.kt | 1 + .../omnivore/ui/library/SearchViewModel.kt | 44 ++--- .../ui/savedItemViews/SavedItemCard.kt | 3 +- .../app/src/main/res/drawable/unarchive.xml | 9 + 7 files changed, 211 insertions(+), 75 deletions(-) create mode 100644 android/Omnivore/app/src/main/res/drawable/unarchive.xml diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt index c26e868a1..acdff92e0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.* import androidx.compose.runtime.* @@ -58,49 +59,147 @@ fun LibraryNavigationBar( } }, actions = { - if (actionsMenuItem != null) { - IconButton(onClick = onSearchClicked) { - Icon( - painter = painterResource(id = R.drawable.archive_outline), - contentDescription = null - ) - } - IconButton(onClick = onSearchClicked) { - Icon( - painter = painterResource(id = R.drawable.tag), - contentDescription = null - ) - } - IconButton(onClick = onSearchClicked) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = null - ) - } - IconButton(onClick = onSettingsIconClick) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null - ) - } - } else { - IconButton(onClick = onSearchClicked) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = null - ) - } + actionsMenuItem?.let { + IconButton(onClick = { + savedItemViewModel.handleSavedItemAction( + it.savedItemId, + if (it.isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive + ) }) { + if (it.isArchived) { + Icon( + painter = painterResource(id = R.drawable.unarchive), + contentDescription = null + ) + } else { + Icon( + painter = painterResource(id = R.drawable.archive_outline), + contentDescription = null + ) + } + } + IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItemId, SavedItemAction.EditLabels) }) { + Icon( + painter = painterResource(id = R.drawable.tag), + contentDescription = null + ) + } + IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItemId, SavedItemAction.Delete) }) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null + ) + } +// IconButton(onClick = onSettingsIconClick) { +// Icon( +// imageVector = Icons.Default.MoreVert, +// contentDescription = null +// ) +// } + } ?: run { + IconButton(onClick = onSearchClicked) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null + ) + } - IconButton(onClick = onSettingsIconClick) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null - ) + IconButton(onClick = onSettingsIconClick) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } } - } } ) } +// +//@OptIn(ExperimentalMaterial3Api::class) +//@Composable +//fun LibraryBottomBar( +// savedItemViewModel: SavedItemViewModel, +// onSearchClicked: () -> Unit, +// onSettingsIconClick: () -> Unit +//) { +// val actionsMenuItem: SavedItemCardData? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) +// +// BottomAppBar( +//// colors = TopAppBarDefaults.topAppBarColors( +//// containerColor = if (actionsMenuItem == null) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.surfaceVariant +//// ), +//// trailingIcon = { +//// if (actionsMenuItem != null) { +//// IconButton(onClick = { +//// savedItemViewModel.actionsMenuItemLiveData.postValue(null) +//// }) { +//// Icon( +//// imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack, +//// modifier = Modifier, +//// contentDescription = "Back" +//// ) +//// } +//// } +//// }, +// floatingActionButton = { +// IconButton(onClick = onSearchClicked) { +// Icon( +// imageVector = Icons.Outlined.Close, +// contentDescription = null +// ) +// } +// }, +// actions = { +// actionsMenuItem?.let { +// IconButton(onClick = onSearchClicked) { +// if (it.isArchived) { +// Icon( +// painter = painterResource(id = R.drawable.unarchive), +// contentDescription = null +// ) +// } else { +// Icon( +// painter = painterResource(id = R.drawable.archive_outline), +// contentDescription = null +// ) +// } +// } +// IconButton(onClick = onSearchClicked) { +// Icon( +// painter = painterResource(id = R.drawable.tag), +// contentDescription = null +// ) +// } +// IconButton(onClick = onSearchClicked) { +// Icon( +// imageVector = Icons.Outlined.Delete, +// contentDescription = null +// ) +// } +// IconButton(onClick = onSettingsIconClick) { +// Icon( +// imageVector = Icons.Default.MoreVert, +// contentDescription = null +// ) +// } +// } ?: run { +// IconButton(onClick = onSearchClicked) { +// Icon( +// imageVector = Icons.Filled.Search, +// contentDescription = null +// ) +// } +// +// IconButton(onClick = onSettingsIconClick) { +// Icon( +// imageVector = Icons.Default.MoreVert, +// contentDescription = null +// ) +// } +// } +// } +// ) +//} + @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) 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 516883d48..2b293e348 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 @@ -2,30 +2,44 @@ package app.omnivore.omnivore.ui.library import android.content.Intent import android.util.Log +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.* 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.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import app.omnivore.omnivore.R import app.omnivore.omnivore.Routes +import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels import app.omnivore.omnivore.ui.components.LabelsSelectionSheet 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.SaveSheetActivityBase import kotlinx.coroutines.flow.distinctUntilChanged @@ -35,6 +49,9 @@ fun LibraryView( libraryViewModel: LibraryViewModel, navController: NavHostController ) { + + val actionsMenuItem: SavedItemCardData? by libraryViewModel.actionsMenuItemLiveData.observeAsState(null) + Scaffold( topBar = { LibraryNavigationBar( @@ -43,19 +60,19 @@ fun LibraryView( onSettingsIconClick = { navController.navigate(Routes.Settings.route) } ) - } + }, ) { paddingValues -> - LibraryViewContent( - libraryViewModel, - modifier = Modifier - .padding( - top = paddingValues.calculateTopPadding() - ) - ) + LibraryViewContent( + libraryViewModel, + modifier = Modifier + .padding( + top = paddingValues.calculateTopPadding() + ) + ) } } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { val context = LocalContext.current @@ -66,7 +83,9 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { onRefresh = { libraryViewModel.refresh() } ) - val cardsData: List by libraryViewModel.itemsLiveData.observeAsState(listOf()) + val cardsData: List by libraryViewModel.itemsLiveData.observeAsState( + listOf() + ) Box( modifier = Modifier @@ -91,12 +110,18 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { cardData = cardDataWithLabels.cardData, labels = cardDataWithLabels.labels, onClickHandler = { - val activityClass = if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + val activityClass = + if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java val intent = Intent(context, activityClass) intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug) context.startActivity(intent) }, - actionHandler = { libraryViewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) } + actionHandler = { + libraryViewModel.handleSavedItemAction( + cardDataWithLabels.cardData.savedItemId, + it + ) + } ) } } @@ -110,7 +135,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { libraryViewModel.loadUsingSearchAPI() } } - + PullRefreshIndicator( refreshing = libraryViewModel.isRefreshing, state = pullRefreshState, 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 6dd2f60da..363d8442b 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 @@ -248,7 +248,7 @@ class LibraryViewModel @Inject constructor( } } - fun handleSavedItemAction(itemID: String, action: SavedItemAction) { + override fun handleSavedItemAction(itemID: String, action: SavedItemAction) { when (action) { SavedItemAction.Delete -> { viewModelScope.launch { @@ -270,6 +270,7 @@ class LibraryViewModel @Inject constructor( showLabelsSelectionSheetLiveData.value = true } } + actionsMenuItemLiveData.postValue(null) } fun updateSavedItemLabels(savedItemID: String, labels: List) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt index fc0f9917c..2c51fdb5b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt @@ -8,4 +8,5 @@ interface SavedItemViewModel { val actionsMenuItemLiveData: MutableLiveData get() = MutableLiveData(null) + fun handleSavedItemAction(itemID: String, action: SavedItemAction) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt index bbaf0444d..47dea3c67 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt @@ -144,28 +144,28 @@ class SearchViewModel @Inject constructor( isRefreshing.postValue(false) } - fun handleSavedItemAction(itemID: String, action: SavedItemAction) { -// when (action) { -// SavedItemAction.Delete -> { -// viewModelScope.launch { -// dataService.deleteSavedItem(itemID) -// } -// } -// SavedItemAction.Archive -> { -// viewModelScope.launch { -// dataService.archiveSavedItem(itemID) -// } -// } -// SavedItemAction.Unarchive -> { -// viewModelScope.launch { -// dataService.unarchiveSavedItem(itemID) -// } -// } -// SavedItemAction.EditLabels -> { -// labelsSelectionCurrentItemLiveData.value = itemID -// showLabelsSelectionSheetLiveData.value = true -// } -// } + override fun handleSavedItemAction(itemID: String, action: SavedItemAction) { + when (action) { + SavedItemAction.Delete -> { + viewModelScope.launch { + dataService.deleteSavedItem(itemID) + } + } + SavedItemAction.Archive -> { + viewModelScope.launch { + dataService.archiveSavedItem(itemID) + } + } + SavedItemAction.Unarchive -> { + viewModelScope.launch { + dataService.unarchiveSavedItem(itemID) + } + } + SavedItemAction.EditLabels -> { + // TODO + } + } + actionsMenuItemLiveData.postValue(null) } fun updateSavedItemLabels(savedItemID: String, labels: List) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index f5b7cc8bb..4db46edd1 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -41,13 +41,14 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar onClick = onClickHandler, onLongClick = { savedItemViewModel.actionsMenuItemLiveData.postValue(cardData) } ) + .fillMaxWidth() ) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top, modifier = Modifier .fillMaxWidth() - .padding(15.dp) + .padding(20.dp) .background(if (isMenuExpanded) Color.LightGray else Color.Transparent) ) { Column( diff --git a/android/Omnivore/app/src/main/res/drawable/unarchive.xml b/android/Omnivore/app/src/main/res/drawable/unarchive.xml new file mode 100644 index 000000000..922a5617d --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/unarchive.xml @@ -0,0 +1,9 @@ + + + From 52f302f5b0c9dbb445c5bd8a5269743ef3b8b300 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 21 Apr 2023 12:33:23 +0800 Subject: [PATCH 19/24] Fetch all label and highlight info when doing library updates --- android/Omnivore/app/src/main/graphql/Search.graphql | 7 ++----- android/Omnivore/app/src/main/graphql/UpdatesSince.graphql | 7 ++----- .../java/app/omnivore/omnivore/dataService/LibrarySync.kt | 6 +++--- .../java/app/omnivore/omnivore/networking/SearchQuery.kt | 6 +++--- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/android/Omnivore/app/src/main/graphql/Search.graphql b/android/Omnivore/app/src/main/graphql/Search.graphql index 9a397ce81..e35140fc2 100644 --- a/android/Omnivore/app/src/main/graphql/Search.graphql +++ b/android/Omnivore/app/src/main/graphql/Search.graphql @@ -22,13 +22,10 @@ query Search($after: String, $first: Int, $query: String) { originalArticleUrl uploadFileId labels { - id - name - color + ...LabelFields } highlights { - id - type + ...HighlightFields } pageId shortId diff --git a/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql index d1a7434bd..aa93b28d5 100644 --- a/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql +++ b/android/Omnivore/app/src/main/graphql/UpdatesSince.graphql @@ -24,13 +24,10 @@ query UpdatesSince($after: String, $first: Int, $since: Date!) { originalArticleUrl uploadFileId labels { - id - name - color + ...LabelFields } highlights { - id - type + ...HighlightFields } pageId shortId diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index adb4fe09c..cb2e546c7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -76,9 +76,9 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S for (item in syncResult.items) { val itemLabels = (item.labels ?: listOf()).map { SavedItemLabel( - savedItemLabelId = it.id, - name = it.name, - color = it.color, + savedItemLabelId = it.labelFields.id, + name = it.labelFields.name, + color = it.labelFields.color, createdAt = null, labelDescription = null ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt index 45bb98d1a..2662789f8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt @@ -91,9 +91,9 @@ suspend fun Networker.search( ), labels = (it.node.labels ?: listOf()).map { label -> SavedItemLabel( - savedItemLabelId = label.id, - name = label.name, - color = label.color, + savedItemLabelId = label.labelFields.id, + name = label.labelFields.name, + color = label.labelFields.color, createdAt = null, labelDescription = null ) From 0f64f3c1024192aa64681ea75ee669b35eb61867 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 21 Apr 2023 13:36:26 +0800 Subject: [PATCH 20/24] fetch highlights and labels with library sync --- .../omnivore/dataService/LibrarySync.kt | 12 +++- .../omnivore/networking/SearchQuery.kt | 55 +++++++------------ .../omnivore/networking/TypeaheadSearch.kt | 39 +++++++++++++ .../persistence/entities/SavedItem.kt | 1 + .../omnivore/ui/library/LibraryViewModel.kt | 31 +---------- .../omnivore/ui/library/SearchViewModel.kt | 28 +++++----- 6 files changed, 86 insertions(+), 80 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/TypeaheadSearch.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index cb2e546c7..0ec769f2b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -7,9 +7,15 @@ import app.omnivore.omnivore.persistence.entities.* suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult { val searchResult = networker.search(cursor = cursor, limit = 10, query = query) - val savedItems = searchResult.items.map { it.item } + val savedItems = searchResult.items.map { + SavedItemWithLabelsAndHighlights( + savedItem = it.item, + labels = it.labels, + highlights = it.highlights, + ) + } - db.savedItemDao().insertAll(savedItems) + db.savedItemDao().insertAll(savedItems.map { it.savedItem }) val labels: MutableList = mutableListOf() val crossRefs: MutableList = mutableListOf() @@ -156,7 +162,7 @@ data class SearchResult( val hasError: Boolean, val hasMoreItems: Boolean, val count: Int, - val savedItems: List, + val savedItems: List, val cursor: String? ) { companion object { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt index 2662789f8..8f21c8e73 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/SearchQuery.kt @@ -1,18 +1,12 @@ package app.omnivore.omnivore.networking +import androidx.room.PrimaryKey import app.omnivore.omnivore.graphql.generated.SearchQuery import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery -import app.omnivore.omnivore.persistence.entities.SavedItem -import app.omnivore.omnivore.persistence.entities.SavedItemCardData -import app.omnivore.omnivore.persistence.entities.SavedItemLabel -import app.omnivore.omnivore.persistence.entities.TypeaheadCardData +import app.omnivore.omnivore.models.ServerSyncStatus +import app.omnivore.omnivore.persistence.entities.* import com.apollographql.apollo3.api.Optional -data class SearchQueryResponse( - val cursor: String?, - val cardsData: List -) - data class LibrarySearchQueryResponse( val cursor: String?, val items: List @@ -20,34 +14,10 @@ data class LibrarySearchQueryResponse( data class LibrarySearchItem( val item: SavedItem, - val labels: List + val labels: List, + val highlights: List ) -suspend fun Networker.typeaheadSearch( - query: String -): SearchQueryResponse { - try { - val result = authenticatedApolloClient().query( - TypeaheadSearchQuery(query) - ).execute() - - val itemList = result.data?.typeaheadSearch?.onTypeaheadSearchSuccess?.items ?: listOf() - - val cardsData = itemList.map { - TypeaheadCardData( - savedItemId = it.id, - slug = it.slug, - title = it.title, - isArchived = false, - ) - } - - return SearchQueryResponse(null, cardsData) - } catch (e: java.lang.Exception) { - return SearchQueryResponse(null, listOf()) - } -} - suspend fun Networker.search( cursor: String? = null, limit: Int = 15, @@ -97,6 +67,21 @@ suspend fun Networker.search( createdAt = null, labelDescription = null ) + }, + highlights = (it.node.highlights ?: listOf()).map { highlight -> + Highlight( + highlightId = highlight.highlightFields.id, + annotation = highlight.highlightFields.annotation, + createdByMe = highlight.highlightFields.createdByMe, + patch = highlight.highlightFields.patch, + prefix = highlight.highlightFields.prefix, + quote = highlight.highlightFields.quote, + serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue, + shortId = highlight.highlightFields.shortId, + suffix = highlight.highlightFields.suffix, + updatedAt = highlight.highlightFields.updatedAt as String?, + createdAt = null, + ) } ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/TypeaheadSearch.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/TypeaheadSearch.kt new file mode 100644 index 000000000..48a61bafd --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/networking/TypeaheadSearch.kt @@ -0,0 +1,39 @@ +package app.omnivore.omnivore.networking + +import app.omnivore.omnivore.graphql.generated.SearchQuery +import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery +import app.omnivore.omnivore.persistence.entities.SavedItem +import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.persistence.entities.TypeaheadCardData +import com.apollographql.apollo3.api.Optional + +data class SearchQueryResponse( + val cursor: String?, + val cardsData: List +) + +suspend fun Networker.typeaheadSearch( + query: String +): SearchQueryResponse { + try { + val result = authenticatedApolloClient().query( + TypeaheadSearchQuery(query) + ).execute() + + val itemList = result.data?.typeaheadSearch?.onTypeaheadSearchSuccess?.items ?: listOf() + + val cardsData = itemList.map { + TypeaheadCardData( + savedItemId = it.id, + slug = it.slug, + title = it.title, + isArchived = false, + ) + } + + return SearchQueryResponse(null, cardsData) + } catch (e: java.lang.Exception) { + return SearchQueryResponse(null, listOf()) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index 9ad6c5cff..f61165863 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -4,6 +4,7 @@ import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.room.* import app.omnivore.omnivore.BuildConfig +import app.omnivore.omnivore.graphql.generated.SearchQuery import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.ui.library.SavedItemSortFilter import java.util.* 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 363d8442b..dd2313693 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 @@ -141,39 +141,13 @@ class LibraryViewModel @Inject constructor( } result.savedItems.map { - val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.slug) + val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug) if (!isSavedInDB) { delay(2000) - contentRequestChannel.send(it.slug) + contentRequestChannel.send(it.savedItem.slug) } } - - val newItems = result.savedItems.map { - SavedItemCardDataWithLabels( - cardData = SavedItemCardData( - savedItemId = it.savedItemId, - slug = it.slug, - publisherURLString = it.publisherURLString, - title = it.title, - author = it.author, - imageURLString = it.imageURLString, - isArchived = it.isArchived, - pageURLString = it.pageURLString, - contentReader = it.contentReader, - savedAt = it.savedAt, - readingProgress = it.readingProgress, - wordsCount = it.wordsCount - ), - labels = listOf() - ) - } - - itemsLiveData.value?.let{ - itemsLiveData.postValue(newItems + it) - } ?: run { - itemsLiveData.postValue(newItems) - } } } } @@ -203,6 +177,7 @@ class LibraryViewModel @Inject constructor( suspend fun handleFilterChanges() { if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { + println("PERFORMING A FILTER CHANGE") itemsLiveDataInternal = dataService.libraryLiveData(appliedFilterLiveData.value!!, appliedSortFilterLiveData.value!!, activeLabelsLiveData.value ?: listOf()) itemsLiveData.removeSource(itemsLiveDataInternal) itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt index 47dea3c67..02978e380 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt @@ -80,29 +80,29 @@ class SearchViewModel @Inject constructor( } result.savedItems.map { - val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.slug) + val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug) if (!isSavedInDB) { delay(2000) - contentRequestChannel.send(it.slug) + contentRequestChannel.send(it.savedItem.slug) } } val newItems = result.savedItems.map { SavedItemCardDataWithLabels( cardData = SavedItemCardData( - savedItemId = it.savedItemId, - slug = it.slug, - publisherURLString = it.publisherURLString, - title = it.title, - author = it.author, - imageURLString = it.imageURLString, - isArchived = it.isArchived, - pageURLString = it.pageURLString, - contentReader = it.contentReader, - savedAt = it.savedAt, - readingProgress = it.readingProgress, - wordsCount = it.wordsCount + savedItemId = it.savedItem.savedItemId, + slug = it.savedItem.slug, + publisherURLString = it.savedItem.publisherURLString, + title = it.savedItem.title, + author = it.savedItem.author, + imageURLString = it.savedItem.imageURLString, + isArchived = it.savedItem.isArchived, + pageURLString = it.savedItem.pageURLString, + contentReader = it.savedItem.contentReader, + savedAt = it.savedItem.savedAt, + readingProgress = it.savedItem.readingProgress, + wordsCount = it.savedItem.wordsCount ), labels = listOf() ) From c76d7144804d979f05fc4815aa5937f4d2472cda Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 21 Apr 2023 15:44:23 +0800 Subject: [PATCH 21/24] More consistent padding on library cells --- .../omnivore/ui/savedItemViews/SavedItemCard.kt | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index 4db46edd1..2cb33a2cb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -48,7 +48,7 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar verticalAlignment = Alignment.Top, modifier = Modifier .fillMaxWidth() - .padding(20.dp) + .padding(10.dp) .background(if (isMenuExpanded) Color.LightGray else Color.Transparent) ) { Column( @@ -109,7 +109,7 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(start = 5.dp) + .padding(start = 10.dp, bottom = 5.dp, end = 10.dp) ) { items(labels.sortedBy { it.name }) { label -> val chipColors = LabelChipColors.fromHex(label.color) @@ -123,20 +123,13 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar labelColor = chipColors.textColor, iconContentColor = chipColors.textColor ), - modifier = Modifier - .padding(horizontal = 4.dp) +// modifier = Modifier +// .padding(horizontal = 4.dp) ) } } Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) - - SavedItemContextMenu( - isExpanded = isMenuExpanded, - isArchived = cardData.isArchived, - onDismiss = { isMenuExpanded = false }, - actionHandler = actionHandler - ) } } From 64ded2481e7daba01b1b84ba49f7b437d5cd964d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 21 Apr 2023 20:14:03 +0800 Subject: [PATCH 22/24] Always take up space for the image --- .../ui/savedItemViews/SavedItemCard.kt | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index 2cb33a2cb..2c79a56c8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.material3.SuggestionChipDefaults.elevatedSuggestionChipColors import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -31,10 +32,12 @@ import coil.compose.rememberAsyncImagePainter ) @Composable fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCardData, labels: List, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { - var isMenuExpanded by remember { mutableStateOf(false) } - val publisherDisplayName = cardData.publisherDisplayName() val listState = rememberLazyListState() + val actionsMenuItem: SavedItemCardData? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) + var isFocused = actionsMenuItem?.savedItemId == cardData.savedItemId + + Column( modifier = Modifier .combinedClickable( @@ -49,7 +52,7 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar modifier = Modifier .fillMaxWidth() .padding(10.dp) - .background(if (isMenuExpanded) Color.LightGray else Color.Transparent) + .background(Color.Transparent) ) { Column( verticalArrangement = Arrangement.spacedBy(5.dp), @@ -82,26 +85,17 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar overflow = TextOverflow.Ellipsis ) } -// -// if (publisherDisplayName != null) { -// Text( -// text = publisherDisplayName, -// style = MaterialTheme.typography.bodyMedium, -// maxLines = 1, -// overflow = TextOverflow.Ellipsis -// ) -// } } - if (cardData.imageURLString != null) { - Image( - painter = rememberAsyncImagePainter(cardData.imageURLString), - contentDescription = "Image associated with saved item", - modifier = Modifier - .size(55.dp, 73.dp) - .clip(RoundedCornerShape(10.dp)) - ) - } + Image( + painter = rememberAsyncImagePainter(cardData.imageURLString), + contentDescription = "Image associated with saved item", + modifier = Modifier + .size(55.dp, 73.dp) + .clip(RoundedCornerShape(10.dp)) + .defaultMinSize(minWidth = 55.dp, minHeight = 73.dp) + .clip(RoundedCornerShape(10.dp)) + ) } LazyRow( @@ -123,9 +117,9 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar labelColor = chipColors.textColor, iconContentColor = chipColors.textColor ), -// modifier = Modifier -// .padding(horizontal = 4.dp) + modifier = Modifier.padding(end = 5.dp) ) + } } @@ -175,8 +169,9 @@ fun readingProgress(item: SavedItemCardData): String { } return "" } -// + //var highlightsText: String { +// item.hig ?.let { // if let highlights = item.highlights, highlights.count > 0 { // let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count) // if item.wordsCount > 0 { From aaf27b0631e2e524fa1a4044ff15d1d6373d8bd8 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 21 Apr 2023 20:47:52 +0800 Subject: [PATCH 23/24] Persist highlights while syncing --- .../omnivore/dataService/LibrarySync.kt | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt index 0ec769f2b..d98fd27b2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/dataService/LibrarySync.kt @@ -1,6 +1,8 @@ package app.omnivore.omnivore.dataService import android.util.Log +import androidx.room.PrimaryKey +import app.omnivore.omnivore.models.ServerSyncStatus import app.omnivore.omnivore.networking.* import app.omnivore.omnivore.persistence.entities.* @@ -46,7 +48,8 @@ suspend fun DataService.librarySearch(cursor: String?, query: String): SearchRes } suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult { - val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) ?: return SavedItemSyncResult.errorResult + val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since) + ?: return SavedItemSyncResult.errorResult val savedItems = syncResult.items.map { SavedItem( @@ -93,7 +96,10 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S labels.addAll(itemLabels) val newCrossRefs = itemLabels.map { - SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = item.id) + SavedItemAndSavedItemLabelCrossRef( + savedItemLabelId = it.savedItemLabelId, + savedItemId = item.id + ) } crossRefs.addAll(newCrossRefs) @@ -102,6 +108,37 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S db.savedItemLabelDao().insertAll(labels) db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs) + // Persist Highlights + db.highlightDao().insertAll(syncResult.items.flatMap { + it.highlights ?: listOf() + }.map { + Highlight( + highlightId = it.highlightFields.id, + annotation = it.highlightFields.annotation, + createdByMe = it.highlightFields.createdByMe, + markedForDeletion = false, + patch = it.highlightFields.patch, + prefix = it.highlightFields.prefix, + quote = it.highlightFields.quote, + serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue, + shortId = it.highlightFields.shortId, + suffix = it.highlightFields.suffix, + createdAt = null, + updatedAt = it.highlightFields.updatedAt as String?, + ) + }) + + val highlightCrossRefs = syncResult.items.flatMap { + val savedItem = it + (savedItem.highlights ?: listOf()).map { + Pair(it, savedItem.id) + } + }.map { + SavedItemAndHighlightCrossRef(highlightId = it.first.highlightFields.id, savedItemId = it.second) + } + + db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs) + Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since") return SavedItemSyncResult( From 32f5bd240703c1c2e61b35eefe99da82136dc585 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 24 Apr 2023 18:26:30 +0800 Subject: [PATCH 24/24] Use a single SQL query for loading library data --- android/Omnivore/app/build.gradle | 4 +- .../persistence/entities/SavedItem.kt | 65 ++++++++++ .../ui/components/LabelsSelectionSheet.kt | 2 +- .../ui/library/LibraryNavigationBar.kt | 13 +- .../omnivore/ui/library/LibraryView.kt | 15 +-- .../omnivore/ui/library/LibraryViewModel.kt | 114 +++++++++++++----- .../omnivore/ui/library/SavedItemViewModel.kt | 5 +- .../omnivore/ui/library/SearchView.kt | 14 +-- .../omnivore/ui/library/SearchViewModel.kt | 9 +- .../ui/savedItemViews/SavedItemCard.kt | 43 ++++--- 10 files changed, 200 insertions(+), 84 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index a864109ad..67db8bfc6 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -17,8 +17,8 @@ android { applicationId "app.omnivore.omnivore" minSdk 26 targetSdk 33 - versionCode 33 - versionName "0.0.33" + versionCode 44 + versionName "0.0.44" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt index f61165863..38cd552d6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/persistence/entities/SavedItem.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore.persistence.entities import androidx.core.net.toUri import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.room.* import app.omnivore.omnivore.BuildConfig import app.omnivore.omnivore.graphql.generated.SearchQuery @@ -160,9 +161,73 @@ interface SavedItemDao { "ORDER BY publishDate DESC" ) fun getLibraryLiveDataSortedByRecentlyPublished(archiveFilter: Int): LiveData> + + @Transaction + @Query( + "SELECT ${SavedItemQueryConstants.libraryColumns} " + + "FROM SavedItem " + + "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + + "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + + + "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + + "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + + + "WHERE SavedItem.serverSyncStatus != 2 " + + "AND SavedItem.isArchived != :archiveFilter " + + "AND SavedItem.contentReader IN (:allowedContentReaders) " + + "AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " + + "AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " + + + "GROUP BY SavedItem.savedItemId " + + + "ORDER BY \n" + + "CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" + + "CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" + + + "CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" + + "CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC" + ) + fun _filteredLibraryData(archiveFilter: Int, sortKey: String, hasRequiredLabels: Int, hasExcludedLabels: Int, requiredLabels: List, excludedLabels: List, allowedContentReaders: List): LiveData> + + fun filteredLibraryData(archiveFilter: Int, sortKey: String, requiredLabels: List, excludedLabels: List, allowedContentReaders: List): LiveData> { + return _filteredLibraryData( + archiveFilter = archiveFilter, + sortKey = sortKey, + hasRequiredLabels = requiredLabels.size, + hasExcludedLabels = excludedLabels.size, + requiredLabels = requiredLabels, + excludedLabels = excludedLabels, + allowedContentReaders = allowedContentReaders + ) + } } + + object SavedItemQueryConstants { const val columns = "savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress, wordsCount" + const val libraryColumns = "SavedItem.savedItemId, " + + "SavedItem.slug, " + + "SavedItem.createdAt, " + + + "SavedItem.publisherURLString, " + + "SavedItem.title, " + + "SavedItem.author, " + + "SavedItem.imageURLString, " + + "SavedItem.isArchived, " + + "SavedItem.pageURLString, " + + "SavedItem.contentReader, " + + "SavedItem.savedAt, " + + "SavedItem.readingProgress, " + + "SavedItem.readingProgressAnchor, " + + "SavedItem.serverSyncStatus, " + + + "SavedItem.wordsCount, " + + "SavedItemLabel.savedItemLabelId, " + + "SavedItemLabel.name, " + + "SavedItemLabel.color, " + + "Highlight.highlightId, " + + "Highlight.shortId, " + + "Highlight.createdByMe " } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt index cf0ab9623..f441e0172 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/LabelsSelectionSheet.kt @@ -76,7 +76,7 @@ fun LabelsSelectionSheet(viewModel: LibraryViewModel) { isLibraryMode = false, onSave = { if (it != labels) { - viewModel.updateSavedItemLabels(savedItemID = currentSavedItemData.cardData.savedItemId, labels = it) + viewModel.updateSavedItemLabels(savedItemID = currentSavedItemData.savedItem.savedItemId, labels = it) } viewModel.labelsSelectionCurrentItemLiveData.value = null viewModel.showLabelsSelectionSheetLiveData.value = false diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt index acdff92e0..afd43845b 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/LibraryNavigationBar.kt @@ -27,6 +27,7 @@ import androidx.navigation.NavHostController import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels +import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -35,7 +36,7 @@ fun LibraryNavigationBar( onSearchClicked: () -> Unit, onSettingsIconClick: () -> Unit ) { - val actionsMenuItem: SavedItemCardData? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) + val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) TopAppBar( title = { @@ -62,10 +63,10 @@ fun LibraryNavigationBar( actionsMenuItem?.let { IconButton(onClick = { savedItemViewModel.handleSavedItemAction( - it.savedItemId, - if (it.isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive + it.savedItem.savedItemId, + if (it.savedItem.isArchived) SavedItemAction.Unarchive else SavedItemAction.Archive ) }) { - if (it.isArchived) { + if (it.savedItem.isArchived) { Icon( painter = painterResource(id = R.drawable.unarchive), contentDescription = null @@ -77,13 +78,13 @@ fun LibraryNavigationBar( ) } } - IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItemId, SavedItemAction.EditLabels) }) { + IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItem.savedItemId, SavedItemAction.EditLabels) }) { Icon( painter = painterResource(id = R.drawable.tag), contentDescription = null ) } - IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItemId, SavedItemAction.Delete) }) { + IconButton(onClick = { savedItemViewModel.handleSavedItemAction(it.savedItem.savedItemId, SavedItemAction.Delete) }) { Icon( imageVector = Icons.Outlined.Delete, contentDescription = null 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 2b293e348..3d9596578 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 @@ -35,6 +35,7 @@ import app.omnivore.omnivore.R import app.omnivore.omnivore.Routes import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels +import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.ui.components.LabelsSelectionSheet import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard import app.omnivore.omnivore.ui.reader.PDFReaderActivity @@ -49,9 +50,6 @@ fun LibraryView( libraryViewModel: LibraryViewModel, navController: NavHostController ) { - - val actionsMenuItem: SavedItemCardData? by libraryViewModel.actionsMenuItemLiveData.observeAsState(null) - Scaffold( topBar = { LibraryNavigationBar( @@ -83,7 +81,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { onRefresh = { libraryViewModel.refresh() } ) - val cardsData: List by libraryViewModel.itemsLiveData.observeAsState( + val cardsData: List by libraryViewModel.itemsLiveData.observeAsState( listOf() ) @@ -107,18 +105,17 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { items(cardsData) { cardDataWithLabels -> SavedItemCard( savedItemViewModel = libraryViewModel, - cardData = cardDataWithLabels.cardData, - labels = cardDataWithLabels.labels, + savedItem = cardDataWithLabels, onClickHandler = { val activityClass = - if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + if (cardDataWithLabels.savedItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java val intent = Intent(context, activityClass) - intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug) + intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.savedItem.slug) context.startActivity(intent) }, actionHandler = { libraryViewModel.handleSavedItemAction( - cardDataWithLabels.cardData.savedItemId, + cardDataWithLabels.savedItem.savedItemId, it ) } 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 dd2313693..cc4bb85ed 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 @@ -4,10 +4,7 @@ import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import app.omnivore.omnivore.* import app.omnivore.omnivore.dataService.* import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput @@ -38,8 +35,14 @@ class LibraryViewModel @Inject constructor( private var receivedIdx = 0 // Live Data - private var itemsLiveDataInternal = dataService.libraryLiveData(SavedItemFilter.INBOX, SavedItemSortFilter.NEWEST, listOf()) - val itemsLiveData = MediatorLiveData>() + private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData( + archiveFilter = 1, + 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) @@ -47,7 +50,7 @@ class LibraryViewModel @Inject constructor( 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) var hasLoadedInitialFilters = false @@ -62,25 +65,27 @@ class LibraryViewModel @Inject constructor( } } - runBlocking { - datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemFilter)?.let { str -> - try { - val filter = SavedItemFilter.values().first { it.rawValue == str } - appliedFilterLiveData.postValue(filter) - } catch (e: Exception) { - Log.d("error", "invalid filter value stored in datastore repo: $e") - } - } - - datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemSortFilter)?.let { str -> - try { - val filter = SavedItemSortFilter.values().first { it.rawValue == str } - appliedSortFilterLiveData.postValue(filter) - } catch (e: Exception) { - Log.d("error", "invalid sort filter value stored in datastore repo: $e") - } - } - } +// runBlocking { +// datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemFilter)?.let { str -> +// try { +// val filter = SavedItemFilter.values().first { it.rawValue == str } +// appliedFilterLiveData.postValue(filter) +// } catch (e: Exception) { +// Log.d("error", "invalid filter value stored in datastore repo: $e") +// } +// +// datastoreRepo.getString(DatastoreKeys.lastUsedSavedItemSortFilter)?.let { str -> +// try { +// val filter = SavedItemSortFilter.values().first { it.rawValue == str } +// appliedSortFilterLiveData.postValue(filter) +// } catch (e: Exception) { +// Log.d("error", "invalid sort filter value stored in datastore repo: $e") +// } +// +// handleFilterChanges() +// } +// } +// } viewModelScope.launch { handleFilterChanges() @@ -175,11 +180,56 @@ class LibraryViewModel @Inject constructor( } } - suspend fun handleFilterChanges() { + fun sortKey(appliedSortKey: String) { + when(appliedSortKey) { + + } + } + fun handleFilterChanges() { if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) { - println("PERFORMING A FILTER CHANGE") - itemsLiveDataInternal = dataService.libraryLiveData(appliedFilterLiveData.value!!, appliedSortFilterLiveData.value!!, activeLabelsLiveData.value ?: listOf()) - itemsLiveData.removeSource(itemsLiveDataInternal) + val applied = appliedFilterLiveData.value + val sortKey = when (appliedSortFilterLiveData.value) { + SavedItemSortFilter.NEWEST -> "newest" + SavedItemSortFilter.OLDEST -> "oldest" + SavedItemSortFilter.RECENTLY_READ -> "recentlyRead" + SavedItemSortFilter.RECENTLY_PUBLISHED -> "recentlyPublished" + else -> "newest" + } + + val archiveFilter = when (appliedFilterLiveData.value) { + SavedItemFilter.ARCHIVED -> 0 + else -> 1 + } + + val allowedContentReaders = when(appliedFilterLiveData.value) { + SavedItemFilter.FILES -> listOf("PDF", "EPUB") + else -> listOf("WEB", "PDF", "EPUB") + } + + var requiredLabels = when(appliedFilterLiveData.value) { + SavedItemFilter.NEWSLETTERS -> listOf("Newsletter") + else -> (activeLabelsLiveData.value ?: listOf()).map { it.name } + } + activeLabelsLiveData.value?.let { + requiredLabels = requiredLabels + it.map { it.name } + } + + + val excludeLabels = when(appliedFilterLiveData.value) { + SavedItemFilter.READ_LATER -> listOf("Newsletter") + else -> listOf() + } + + val newData = dataService.db.savedItemDao().filteredLibraryData( + archiveFilter = archiveFilter, + sortKey = sortKey, + requiredLabels = requiredLabels, + excludedLabels = excludeLabels, + allowedContentReaders = allowedContentReaders + ) + + itemsLiveData.removeSource(itemsLiveDataInternal) + itemsLiveDataInternal = newData itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue) } } @@ -295,9 +345,9 @@ class LibraryViewModel @Inject constructor( } } - fun currentSavedItemUnderEdit(): SavedItemCardDataWithLabels? { + fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? { labelsSelectionCurrentItemLiveData.value?.let { itemID -> - return itemsLiveData.value?.first { it.cardData.savedItemId == itemID } + return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID } } return null diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt index 2c51fdb5b..1d2728d8a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SavedItemViewModel.kt @@ -2,11 +2,12 @@ package app.omnivore.omnivore.ui.library import androidx.lifecycle.MutableLiveData import app.omnivore.omnivore.persistence.entities.SavedItemCardData +import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights interface SavedItemViewModel { - val actionsMenuItemLiveData: MutableLiveData - get() = MutableLiveData(null) + val actionsMenuItemLiveData: MutableLiveData + get() = MutableLiveData(null) fun handleSavedItemAction(itemID: String, action: SavedItemAction) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt index 7a95a492f..f2aaa71d6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchView.kt @@ -24,6 +24,7 @@ import androidx.navigation.NavHostController import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels +import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity import app.omnivore.omnivore.persistence.entities.TypeaheadCardData import app.omnivore.omnivore.ui.reader.PDFReaderActivity @@ -39,7 +40,7 @@ fun SearchView( val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false) val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true) val searchText: String by viewModel.searchTextLiveData.observeAsState("") - val actionsMenuItem: SavedItemCardData? by viewModel.actionsMenuItemLiveData.observeAsState(null) + val actionsMenuItem: SavedItemWithLabelsAndHighlights? by viewModel.actionsMenuItemLiveData.observeAsState(null) Scaffold( topBar = { @@ -174,7 +175,7 @@ fun SearchViewContent(viewModel: SearchViewModel, modifier: Modifier) { val context = LocalContext.current val listState = rememberLazyListState() - val cardsData: List by viewModel.itemsLiveData.observeAsState(listOf()) + val cardsData: List by viewModel.itemsLiveData.observeAsState(listOf()) LazyColumn( state = listState, @@ -188,15 +189,14 @@ fun SearchViewContent(viewModel: SearchViewModel, modifier: Modifier) { items(cardsData) { cardDataWithLabels -> SavedItemCard( savedItemViewModel = viewModel, - cardData = cardDataWithLabels.cardData, - labels = cardDataWithLabels.labels, + savedItem = cardDataWithLabels, onClickHandler = { - val activityClass = if (cardDataWithLabels.cardData.isPDF()) PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java + val activityClass = if (cardDataWithLabels.savedItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java val intent = Intent(context, activityClass) - intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug) + intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.savedItem.slug) context.startActivity(intent) }, - actionHandler = { viewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) } + actionHandler = { viewModel.handleSavedItemAction(cardDataWithLabels.savedItem.savedItemId, it) } ) } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt index 02978e380..8db931e1a 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/library/SearchViewModel.kt @@ -42,9 +42,9 @@ class SearchViewModel @Inject constructor( val typeaheadMode = MutableLiveData(true) val searchTextLiveData = MutableLiveData("") val searchItemsLiveData = MutableLiveData>(listOf()) - val itemsLiveData = MediatorLiveData>() + val itemsLiveData = MediatorLiveData>() - override val actionsMenuItemLiveData = MutableLiveData(null) + override val actionsMenuItemLiveData = MutableLiveData(null) fun updateSearchText(text: String) { typeaheadMode.postValue(true) @@ -88,7 +88,9 @@ class SearchViewModel @Inject constructor( } } - val newItems = result.savedItems.map { + val newItems = result.savedItems + /* + .map { SavedItemCardDataWithLabels( cardData = SavedItemCardData( savedItemId = it.savedItem.savedItemId, @@ -107,6 +109,7 @@ class SearchViewModel @Inject constructor( labels = listOf() ) } + */ itemsLiveData.value?.let{ itemsLiveData.postValue(newItems + it) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt index 2c79a56c8..2cfc42db2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/savedItemViews/SavedItemCard.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.* import app.omnivore.omnivore.R import app.omnivore.omnivore.persistence.entities.SavedItemCardData import app.omnivore.omnivore.persistence.entities.SavedItemLabel +import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.ui.components.LabelChipColors import app.omnivore.omnivore.ui.library.LibraryViewModel import app.omnivore.omnivore.ui.library.SavedItemAction @@ -31,18 +32,16 @@ import coil.compose.rememberAsyncImagePainter @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ) @Composable -fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCardData, labels: List, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { +fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWithLabelsAndHighlights, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) { val listState = rememberLazyListState() - val actionsMenuItem: SavedItemCardData? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) - var isFocused = actionsMenuItem?.savedItemId == cardData.savedItemId - + val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) Column( modifier = Modifier .combinedClickable( onClick = onClickHandler, - onLongClick = { savedItemViewModel.actionsMenuItemLiveData.postValue(cardData) } + onLongClick = { savedItemViewModel.actionsMenuItemLiveData.postValue(savedItem) } ) .fillMaxWidth() ) { @@ -61,10 +60,10 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar .padding(end = 20.dp) .defaultMinSize(minHeight = 55.dp) ) { - readInfo(item = cardData) + readInfo(item = savedItem) Text( - text = cardData.title, + text = savedItem.savedItem.title, style = TextStyle( fontSize = 18.sp, fontWeight = FontWeight.SemiBold @@ -73,9 +72,9 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar lineHeight = 20.sp ) - if (cardData.author != null && cardData.author != "") { + if (savedItem.savedItem.author != null && savedItem.savedItem.author != "") { Text( - text = byline(cardData), + text = byline(savedItem), style = TextStyle( fontSize = 15.sp, fontWeight = FontWeight.Normal, @@ -88,7 +87,7 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar } Image( - painter = rememberAsyncImagePainter(cardData.imageURLString), + painter = rememberAsyncImagePainter(savedItem.savedItem.imageURLString), contentDescription = "Image associated with saved item", modifier = Modifier .size(55.dp, 73.dp) @@ -105,7 +104,7 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar modifier = Modifier .padding(start = 10.dp, bottom = 5.dp, end = 10.dp) ) { - items(labels.sortedBy { it.name }) { label -> + items(savedItem.labels.sortedBy { it.name }) { label -> val chipColors = LabelChipColors.fromHex(label.color) SuggestionChip( @@ -127,12 +126,12 @@ fun SavedItemCard(savedItemViewModel: SavedItemViewModel, cardData: SavedItemCar } } -fun byline(item: SavedItemCardData): String { - item.author?.let { - return item.author +fun byline(item: SavedItemWithLabelsAndHighlights): String { + item.savedItem.author?.let { + return item.savedItem.author } - val publisherDisplayName = item.publisherDisplayName() + val publisherDisplayName = item.savedItem.publisherDisplayName() publisherDisplayName?.let { return publisherDisplayName } @@ -149,8 +148,8 @@ fun byline(item: SavedItemCardData): String { // return Int64(result) //} -fun estimatedReadingTime(item: SavedItemCardData): String { - item.wordsCount?.let { +fun estimatedReadingTime(item: SavedItemWithLabelsAndHighlights): String { + item.savedItem.wordsCount?.let { if (it > 0) { val readLen = Math.max(1, it / 235) return "$readLen MIN READ • " @@ -159,11 +158,11 @@ fun estimatedReadingTime(item: SavedItemCardData): String { return "" } -fun readingProgress(item: SavedItemCardData): String { +fun readingProgress(item: SavedItemWithLabelsAndHighlights): String { // If there is no wordsCount don't show progress because it will make no sense - item.wordsCount?.let { + item.savedItem.wordsCount?.let { if (it > 0) { - val intVal = item.readingProgress.toInt() + val intVal = item.savedItem.readingProgress.toInt() return "$intVal%" } } @@ -201,7 +200,7 @@ fun readingProgress(item: SavedItemCardData): String { //} @Composable -fun readInfo(item: SavedItemCardData) { +fun readInfo(item: SavedItemWithLabelsAndHighlights) { Row( modifier = Modifier .fillMaxWidth() @@ -223,7 +222,7 @@ fun readInfo(item: SavedItemCardData) { style = TextStyle( fontSize = 11.sp, fontWeight = FontWeight.Medium, - color = if (item.readingProgress > 1) colorResource(R.color.green_55B938) else colorResource(R.color.gray_898989) + color = if (item.savedItem.readingProgress > 1) colorResource(R.color.green_55B938) else colorResource(R.color.gray_898989) ), maxLines = 1, overflow = TextOverflow.Ellipsis