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.
This commit is contained in:
Jackson Harper
2023-04-18 23:21:55 +08:00
parent 70777d323e
commit 9b0941d705
7 changed files with 193 additions and 58 deletions

View File

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

View File

@ -39,6 +39,7 @@ fun LibraryView(
topBar = {
SearchBar(
libraryViewModel = libraryViewModel,
onSearchClicked = { navController.navigate(Routes.Search.route) },
onSettingsIconClick = { navController.navigate(Routes.Settings.route) }
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -43,14 +43,15 @@ fun SavedItemCard(cardData: SavedItemCardData, labels: List<SavedItemLabel>, 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<SavedItemLabel>, 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<SavedItemLabel>, 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<SavedItemLabel>, 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<SavedItemLabel>, onC
)
}
}
fun byline(item: SavedItemCardData): String {
item.author?.let {
return item.author
}
val publisherDisplayName = item.publisherDisplayName()
publisherDisplayName?.let {
return publisherDisplayName
}
return ""
}