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 "" +}