Android: Add link from library

Signed-off-by: Remy Chantenay <remy.chantenay@gmail.com>
This commit is contained in:
Remy Chantenay
2023-11-03 12:28:06 +01:00
parent ac205618b7
commit cf9532bb30
8 changed files with 214 additions and 17 deletions

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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,

View File

@ -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<SavedItemLabel> 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 }
)
}
}
}

View File

@ -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<String?>(null)
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(listOf())

View File

@ -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,
)
}

View File

@ -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)

View File

@ -204,4 +204,14 @@
<string name="settings_view_setting_row_terms_and_conditions">Terms and Conditions</string>
<string name="settings_view_setting_row_manage_account">Manage Account</string>
<string name="settings_view_setting_row_logout">Logout</string>
<!-- AddLinkSheet -->
<string name="add_link_sheet_title">Add Link</string>
<string name="add_link_sheet_text_field_placeholder">Add Link</string>
<string name="add_link_sheet_action_add_link">Add</string>
<string name="add_link_sheet_action_cancel">Cancel</string>
<string name="add_link_sheet_action_paste_from_clipboard">Get from clipboard</string>
<string name="add_link_sheet_invalid_url_error">Invalid URL</string>
<string name="add_link_sheet_save_url_error">Error while saving link!</string>
<string name="add_link_sheet_save_url_success">Link successfully saved!</string>
</resources>