From cf9532bb30af02d0da1ebfcf3a4be5499c5e901b Mon Sep 17 00:00:00 2001 From: Remy Chantenay Date: Fri, 3 Nov 2023 12:28:06 +0100 Subject: [PATCH] Android: Add link from library Signed-off-by: Remy Chantenay --- .../app/omnivore/omnivore/MainActivity.kt | 10 +- .../omnivore/ui/components/AddLinkSheet.kt | 152 ++++++++++++++++++ .../ui/library/LibraryNavigationBar.kt | 8 + .../omnivore/ui/library/LibraryView.kt | 29 ++-- .../omnivore/ui/library/LibraryViewModel.kt | 1 + .../app/omnivore/omnivore/ui/root/RootView.kt | 9 +- .../omnivore/ui/save/SaveViewModel.kt | 12 +- .../app/src/main/res/values/strings.xml | 10 ++ 8 files changed, 214 insertions(+), 17 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/AddLinkSheet.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 83da1a09c..f5d774913 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 @@ -18,6 +18,7 @@ import app.omnivore.omnivore.ui.components.LabelsViewModel 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.save.SaveViewModel import app.omnivore.omnivore.ui.settings.SettingsViewModel import app.omnivore.omnivore.ui.theme.OmnivoreTheme import com.pspdfkit.PSPDFKit @@ -37,6 +38,7 @@ class MainActivity : ComponentActivity() { val settingsViewModel: SettingsViewModel by viewModels() val searchViewModel: SearchViewModel by viewModels() val labelsViewModel: LabelsViewModel by viewModels() + val saveViewModel: SaveViewModel by viewModels() val context = this @@ -57,7 +59,13 @@ class MainActivity : ComponentActivity() { .fillMaxSize() .background(color = Color.Black) ) { - RootView(loginViewModel, searchViewModel, libraryViewModel, settingsViewModel, labelsViewModel) + RootView( + loginViewModel, + searchViewModel, + libraryViewModel, + settingsViewModel, + labelsViewModel, + saveViewModel) } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/AddLinkSheet.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/AddLinkSheet.kt new file mode 100644 index 000000000..12c9736d4 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/components/AddLinkSheet.kt @@ -0,0 +1,152 @@ +package app.omnivore.omnivore.ui.components + +import android.widget.Toast +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Link +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.lifecycle.MutableLiveData +import app.omnivore.omnivore.R +import app.omnivore.omnivore.ui.save.SaveState +import app.omnivore.omnivore.ui.save.SaveViewModel + +@Composable +fun AddLinkSheetContent( + saveViewModel: SaveViewModel, + onCancel: () -> Unit, + onLinkAdded: () -> Unit +) { + + val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + + val clipboardManager: ClipboardManager = LocalClipboardManager.current + val clipboardText = clipboardManager.getText()?.text + + var textFieldValue by remember { mutableStateOf(TextFieldValue("")) } + + fun showToast(msg: String) { + Toast.makeText( + context, + msg, + Toast.LENGTH_SHORT + ).show() + } + + val saveState: SaveState by saveViewModel.saveState.observeAsState(SaveState.NONE) + val isSaving = MutableLiveData(false) + + when (saveState) { + SaveState.NONE -> { + isSaving.value = false + } + SaveState.SAVING -> { + isSaving.value = true + } + SaveState.ERROR -> { + isSaving.value = false + showToast(context.getString(R.string.add_link_sheet_save_url_error)) + } + SaveState.SAVED -> { + isSaving.value = false + showToast(context.getString(R.string.add_link_sheet_save_url_success)) + + onLinkAdded() + } + } + + fun addLink(url: String) { + if (!saveViewModel.validateUrl(url)) { + showToast(context.getString(R.string.add_link_sheet_invalid_url_error)) + return + } + + saveViewModel.saveURL(url) + } + + Surface( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + Column( + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 5.dp) + ) { + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + TextButton(onClick = onCancel) { + Text(text = stringResource(R.string.add_link_sheet_action_cancel)) + } + + Text(stringResource(R.string.add_link_sheet_title), fontWeight = FontWeight.ExtraBold) + + TextButton(onClick = { addLink(textFieldValue.text) }) { + Text(stringResource(R.string.add_link_sheet_action_add_link)) + } + } + + if (isSaving.value == true) { + Spacer(modifier = Modifier.width(16.dp)) + CircularProgressIndicator( + modifier = Modifier + .height(16.dp) + .width(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary + ) + } + + OutlinedTextField( + value = textFieldValue, + placeholder = { Text(stringResource(R.string.add_link_sheet_text_field_placeholder)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + leadingIcon = { Icon(imageVector = Icons.Default.Link, contentDescription = "linkIcon") }, + onValueChange = { textFieldValue = it }, + modifier = Modifier.focusRequester(focusRequester).padding(top = 24.dp).fillMaxWidth() + ) + + if (clipboardText != null) { + Button( + modifier = Modifier.padding(top = 10.dp), + onClick = { + textFieldValue = TextFieldValue( + text = clipboardText, + selection = TextRange(clipboardText.length)) + } + ) { + Text(stringResource(R.string.add_link_sheet_action_paste_from_clipboard)) + } + } + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} 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 14df87883..e0ccd4b35 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 @@ -29,6 +29,7 @@ import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighligh fun LibraryNavigationBar( savedItemViewModel: SavedItemViewModel, onSearchClicked: () -> Unit, + onAddLinkClicked: () -> Unit, onSettingsIconClick: () -> Unit ) { val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null) @@ -101,6 +102,13 @@ fun LibraryNavigationBar( ) } + IconButton(onClick = onAddLinkClicked) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null + ) + } + IconButton(onClick = onSettingsIconClick) { Icon( imageVector = Icons.Default.MoreVert, 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 a41ba1d03..d5e6f1b5b 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,7 +2,6 @@ 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 @@ -11,17 +10,11 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* -import androidx.compose.material.DrawerValue -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.material3.* import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -29,18 +22,18 @@ 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.SavedItemLabel import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights +import app.omnivore.omnivore.ui.components.AddLinkSheetContent import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent import app.omnivore.omnivore.ui.components.LabelsViewModel 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.SaveViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -50,10 +43,12 @@ import kotlinx.coroutines.launch fun LibraryView( libraryViewModel: LibraryViewModel, labelsViewModel: LabelsViewModel, + saveViewModel: SaveViewModel, navController: NavHostController ) { val scaffoldState: ScaffoldState = rememberScaffoldState() val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) + val showAddLinkSheet: Boolean by libraryViewModel.showAddLinkSheetLiveData.observeAsState(false) val coroutineScope = rememberCoroutineScope() val modalBottomSheetState = rememberModalBottomSheetState( @@ -61,7 +56,7 @@ fun LibraryView( confirmStateChange = { it != ModalBottomSheetValue.Hidden } ) - if (showLabelsSelectionSheet) { + if (showLabelsSelectionSheet || showAddLinkSheet) { coroutineScope.launch { modalBottomSheetState.show() } @@ -82,7 +77,7 @@ fun LibraryView( sheetBackgroundColor = Color.Transparent, sheetState = modalBottomSheetState, sheetContent = { - BottomSheetContent(libraryViewModel, labelsViewModel) + BottomSheetContent(libraryViewModel, labelsViewModel, saveViewModel) Spacer(modifier = Modifier.weight(1.0F)) } ) { @@ -92,6 +87,7 @@ fun LibraryView( LibraryNavigationBar( savedItemViewModel = libraryViewModel, onSearchClicked = { navController.navigate(Routes.Search.route) }, + onAddLinkClicked = { libraryViewModel.showAddLinkSheetLiveData.value = true }, onSettingsIconClick = { navController.navigate(Routes.Settings.route) } ) }, @@ -106,8 +102,9 @@ fun LibraryView( } @Composable -fun BottomSheetContent(libraryViewModel: LibraryViewModel, labelsViewModel: LabelsViewModel) { +fun BottomSheetContent(libraryViewModel: LibraryViewModel, labelsViewModel: LabelsViewModel, saveViewModel: SaveViewModel) { val showLabelsSelectionSheet: Boolean by libraryViewModel.showLabelsSelectionSheetLiveData.observeAsState(false) + val showAddLinkSheet: Boolean by libraryViewModel.showAddLinkSheetLiveData.observeAsState(false) val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit() val labels: List by libraryViewModel.savedItemLabelsLiveData.observeAsState(listOf()) @@ -155,6 +152,14 @@ fun BottomSheetContent(libraryViewModel: LibraryViewModel, labelsViewModel: Labe ) } } + } else if (showAddLinkSheet) { + BottomSheetUI { + AddLinkSheetContent( + saveViewModel = saveViewModel, + onCancel = { libraryViewModel.showAddLinkSheetLiveData.value = false }, + onLinkAdded = { libraryViewModel.showAddLinkSheetLiveData.value = false } + ) + } } } 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 86dfab019..355337679 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 @@ -60,6 +60,7 @@ class LibraryViewModel @Inject constructor( val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) val showLabelsSelectionSheetLiveData = MutableLiveData(false) + val showAddLinkSheetLiveData = MutableLiveData(false) val labelsSelectionCurrentItemLiveData = MutableLiveData(null) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() val activeLabelsLiveData = MutableLiveData>(listOf()) 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 8bc32e3bb..a53280a3c 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,9 +13,7 @@ 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.components.LabelsViewModel @@ -23,6 +21,7 @@ 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.save.SaveViewModel import app.omnivore.omnivore.ui.settings.PolicyWebView import app.omnivore.omnivore.ui.settings.SettingsViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -33,7 +32,8 @@ fun RootView( searchViewModel: SearchViewModel, libraryViewModel: LibraryViewModel, settingsViewModel: SettingsViewModel, - labelsViewModel: LabelsViewModel + labelsViewModel: LabelsViewModel, + saveViewModel: SaveViewModel, ) { val hasAuthToken: Boolean by loginViewModel.hasAuthTokenLiveData.observeAsState(false) val systemUiController = rememberSystemUiController() @@ -59,6 +59,7 @@ fun RootView( libraryViewModel = libraryViewModel, settingsViewModel = settingsViewModel, labelsViewModel = labelsViewModel, + saveViewModel = saveViewModel ) } else { WelcomeScreen(viewModel = loginViewModel) @@ -80,6 +81,7 @@ fun PrimaryNavigator( searchViewModel: SearchViewModel, settingsViewModel: SettingsViewModel, labelsViewModel: LabelsViewModel, + saveViewModel: SaveViewModel, ) { val navController = rememberNavController() @@ -89,6 +91,7 @@ fun PrimaryNavigator( libraryViewModel = libraryViewModel, navController = navController, labelsViewModel = labelsViewModel, + saveViewModel = saveViewModel, ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt index 6bb28a50c..f13dc3a65 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore.ui.save import android.content.ContentValues import android.util.Log +import android.util.Patterns import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -52,7 +53,16 @@ class SaveViewModel @Inject constructor( datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) } - fun cleanUrl(text: String): String? { + /** + * Checks whether or not the provided URL is valid. + * @param url The potential URL to validate. + * @return true if valid, false otherwise. + */ + fun validateUrl(url: String): Boolean { + return Patterns.WEB_URL.matcher(url).matches() + } + + private fun cleanUrl(text: String): String? { val pattern = Pattern.compile("\\b(?:https?|ftp)://\\S+") val matcher = pattern.matcher(text) diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index 2867be8e3..e46ae3194 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -204,4 +204,14 @@ Terms and Conditions Manage Account Logout + + + Add Link + Add Link + Add + Cancel + Get from clipboard + Invalid URL + Error while saving link! + Link successfully saved!