From 007c536312e56452431fee3b21cf06526ab740e5 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Thu, 13 Jun 2024 23:52:05 -0500 Subject: [PATCH] Add search bar to drawer --- .../todoroo/astrid/activity/MainActivity.kt | 4 +- .../astrid/activity/MainActivityViewModel.kt | 35 +++++- .../tasks/compose/drawer/TaskListDrawer.kt | 107 +++++++++++------- .../org/tasks/compose/drawer/TasksMenu.kt | 4 + .../tasks/compose/pickers/CalendarPicker.kt | 1 - .../tasks/widget/ShortcutConfigActivity.kt | 2 - .../previews/components/SearchBarPreviews.kt | 37 ++++++ .../SearchableFilterPickerPreviews.kt | 11 +- .../org/tasks/compose/components/SearchBar.kt | 79 +++++++++++++ .../kotlin}/org/tasks/themes/TasksTheme.kt | 0 10 files changed, 229 insertions(+), 51 deletions(-) create mode 100644 kmp/src/androidMain/kotlin/org/tasks/previews/components/SearchBarPreviews.kt rename kmp/src/androidMain/kotlin/org/tasks/previews/{ => pickers}/SearchableFilterPickerPreviews.kt (80%) create mode 100644 kmp/src/commonMain/kotlin/org/tasks/compose/components/SearchBar.kt rename {app/src/main/java => kmp/src/commonMain/kotlin}/org/tasks/themes/TasksTheme.kt (100%) diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt index 1e23a8283..3c113992f 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -115,7 +115,7 @@ class MainActivity : AppCompatActivity() { bottomPadding = WindowInsets.mandatorySystemGestures .asPaddingValues() .calculateBottomPadding(), - items = state.drawerItems, + items = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems, begForMoney = state.begForMoney, isTopAppBar = preferences.isTopAppBar, setFilter = { viewModel.setFilter(it) }, @@ -161,6 +161,8 @@ class MainActivity : AppCompatActivity() { } }, dismiss = { viewModel.setDrawerOpen(false) }, + query = state.menuQuery, + onQueryChange = { viewModel.queryMenu(it) }, ) } } diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt index 15eec9b7d..d092a8e24 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt @@ -62,6 +62,8 @@ class MainActivityViewModel @Inject constructor( val task: Task? = null, val drawerOpen: Boolean = false, val drawerItems: ImmutableList = persistentListOf(), + val searchItems: ImmutableList = persistentListOf(), + val menuQuery: String = "", ) private val _state = MutableStateFlow( @@ -107,7 +109,12 @@ class MainActivityViewModel @Inject constructor( } fun setDrawerOpen(open: Boolean) { - _state.update { it.copy(drawerOpen = open) } + _state.update { + it.copy( + drawerOpen = open, + menuQuery = if (!open) "" else it.menuQuery, + ) + } } init { @@ -152,6 +159,27 @@ class MainActivityViewModel @Inject constructor( } } .let { filters -> _state.update { it.copy(drawerItems = filters.toPersistentList()) } } + val query = _state.value.menuQuery + filterProvider + .allFilters() + .filter { it.title!!.contains(query, ignoreCase = true) } + .map { item -> + DrawerItem.Filter( + title = item.title ?: "", + icon = getIcon(item), + color = getColor(item), + count = item.count.takeIf { it != NO_COUNT } ?: try { + taskDao.count(item) + } catch (e: Exception) { + Timber.e(e) + 0 + }, + selected = item.areItemsTheSame(selected), + shareCount = if (item is CaldavFilter) item.principals else 0, + type = { item }, + ) + } + .let { filters -> _state.update { it.copy(searchItems = filters.toPersistentList()) } } } private fun getColor(filter: Filter): Int { @@ -202,4 +230,9 @@ class MainActivityViewModel @Inject constructor( fun setTask(task: Task?) { _state.update { it.copy(task = task) } } + + fun queryMenu(query: String) { + _state.update { it.copy(menuQuery = query) } + updateFilters() + } } diff --git a/app/src/main/java/org/tasks/compose/drawer/TaskListDrawer.kt b/app/src/main/java/org/tasks/compose/drawer/TaskListDrawer.kt index 2b512d7eb..099835eec 100644 --- a/app/src/main/java/org/tasks/compose/drawer/TaskListDrawer.kt +++ b/app/src/main/java/org/tasks/compose/drawer/TaskListDrawer.kt @@ -16,17 +16,18 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.ContentAlpha import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.material.icons.outlined.PeopleOutline import androidx.compose.material.icons.outlined.PermIdentity +import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.SyncProblem import androidx.compose.material3.Divider import androidx.compose.material3.Icon @@ -34,7 +35,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate @@ -49,6 +52,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import org.tasks.R import org.tasks.Tasks.Companion.IS_GENERIC +import org.tasks.compose.components.SearchBar import org.tasks.extensions.formatNumber import org.tasks.filters.FilterImpl import org.tasks.filters.NavigationDrawerSubheader @@ -63,7 +67,14 @@ fun TaskListDrawer( onDrawerAction: (DrawerAction) -> Unit, onAddClick: (DrawerItem.Header) -> Unit, onErrorClick: () -> Unit, + query: String, + onQueryChange: (String) -> Unit, ) { + val searching by remember (query) { + derivedStateOf { + query.isNotBlank() + } + } LazyColumn( modifier = Modifier .padding(bottom = bottomPadding) @@ -73,7 +84,33 @@ fun TaskListDrawer( stiffness = Spring.StiffnessMedium ) ) + .imePadding() ) { + item { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + SearchBar( + modifier = Modifier + .padding(start = 8.dp, bottom = 4.dp) + .weight(1f), + text = query, + onTextChange = { onQueryChange(it) }, + placeHolder = stringResource(id = R.string.TLA_menu_search), + onCloseClicked = { onQueryChange("") }, + onSearchClicked = { + // TODO: close keyboard + }, + ) + IconButton(onClick = { onDrawerAction(DrawerAction.SETTINGS) }) { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = stringResource(id = R.string.TLA_menu_settings), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } + } items(items = filters) { when (it) { is DrawerItem.Filter -> FilterItem( @@ -89,48 +126,42 @@ fun TaskListDrawer( ) } } - item { - Divider(modifier = Modifier.fillMaxWidth()) - } - if (begForMoney) { + if (!searching) { + item { + Divider(modifier = Modifier.fillMaxWidth()) + } + if (begForMoney) { + item { + MenuAction( + icon = R.drawable.ic_outline_attach_money_24px, + title = if (IS_GENERIC) R.string.TLA_menu_donate else R.string.name_your_price + ) { + onDrawerAction(DrawerAction.PURCHASE) + } + } + } item { MenuAction( - icon = R.drawable.ic_outline_attach_money_24px, - title = if (IS_GENERIC) R.string.TLA_menu_donate else R.string.name_your_price + icon = R.drawable.ic_outline_edit_24px, + title = R.string.manage_drawer ) { - onDrawerAction(DrawerAction.PURCHASE) + onDrawerAction(DrawerAction.CUSTOMIZE_DRAWER) } } - } - item { - MenuAction( - icon = R.drawable.ic_outline_edit_24px, - title = R.string.manage_drawer - ) { - onDrawerAction(DrawerAction.CUSTOMIZE_DRAWER) - } - } - item { - MenuAction( - icon = R.drawable.ic_outline_settings_24px, - title = R.string.TLA_menu_settings - ) { - onDrawerAction(DrawerAction.SETTINGS) - } - } - item { - MenuAction( - icon = R.drawable.ic_outline_help_outline_24px, - title = R.string.help_and_feedback - ) { - onDrawerAction(DrawerAction.HELP_AND_FEEDBACK) + item { + MenuAction( + icon = R.drawable.ic_outline_help_outline_24px, + title = R.string.help_and_feedback + ) { + onDrawerAction(DrawerAction.HELP_AND_FEEDBACK) + } } } } } @Composable -private fun FilterItem( +internal fun FilterItem( item: DrawerItem.Filter, onClick: () -> Unit, ) { @@ -161,9 +192,7 @@ private fun FilterItem( else -> Icons.Outlined.PeopleOutline }, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface.copy( - alpha = ContentAlpha.medium - ), + tint = MaterialTheme.colorScheme.onSurface, ) } Box( @@ -207,12 +236,12 @@ private fun DrawerIcon(icon: Int, color: Int = 0) { tint = when (color) { 0 -> MaterialTheme.colorScheme.onSurface else -> Color(color) - }.copy(alpha = ContentAlpha.medium) + } ) } @Composable -private fun HeaderItem( +internal fun HeaderItem( item: DrawerItem.Header, canAdd: Boolean, toggleCollapsed: () -> Unit, @@ -289,6 +318,7 @@ private fun MenuRow( fun MenuPreview() { TasksTheme { TaskListDrawer( + begForMoney = true, filters = persistentListOf( DrawerItem.Filter( title = "My Tasks", @@ -313,9 +343,10 @@ fun MenuPreview() { ), onClick = {}, onDrawerAction = {}, - begForMoney = true, onAddClick = {}, onErrorClick = {}, + query = "", + onQueryChange = {}, ) } } diff --git a/app/src/main/java/org/tasks/compose/drawer/TasksMenu.kt b/app/src/main/java/org/tasks/compose/drawer/TasksMenu.kt index 3035805b8..74fc910f8 100644 --- a/app/src/main/java/org/tasks/compose/drawer/TasksMenu.kt +++ b/app/src/main/java/org/tasks/compose/drawer/TasksMenu.kt @@ -42,6 +42,8 @@ fun TasksMenu( toggleCollapsed: (NavigationDrawerSubheader) -> Unit, addFilter: (NavigationDrawerSubheader) -> Unit, dismiss: () -> Unit, + query: String, + onQueryChange: (String) -> Unit, ) { var expanded by remember { mutableStateOf(false) } val skipPartiallyExpanded = remember(expanded) { @@ -128,6 +130,8 @@ fun TasksMenu( onErrorClick = { context.startActivity(Intent(context, MainPreferences::class.java)) }, + query = query, + onQueryChange = onQueryChange, ) } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/pickers/CalendarPicker.kt b/app/src/main/java/org/tasks/compose/pickers/CalendarPicker.kt index 43431ce15..63b09242a 100644 --- a/app/src/main/java/org/tasks/compose/pickers/CalendarPicker.kt +++ b/app/src/main/java/org/tasks/compose/pickers/CalendarPicker.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/org/tasks/widget/ShortcutConfigActivity.kt b/app/src/main/java/org/tasks/widget/ShortcutConfigActivity.kt index 620193a86..2053ece9c 100644 --- a/app/src/main/java/org/tasks/widget/ShortcutConfigActivity.kt +++ b/app/src/main/java/org/tasks/widget/ShortcutConfigActivity.kt @@ -3,9 +3,7 @@ package org.tasks.widget import android.os.Bundle import android.view.View import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.Toolbar -import androidx.core.content.IntentCompat import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat diff --git a/kmp/src/androidMain/kotlin/org/tasks/previews/components/SearchBarPreviews.kt b/kmp/src/androidMain/kotlin/org/tasks/previews/components/SearchBarPreviews.kt new file mode 100644 index 000000000..eb26617bc --- /dev/null +++ b/kmp/src/androidMain/kotlin/org/tasks/previews/components/SearchBarPreviews.kt @@ -0,0 +1,37 @@ +package org.tasks.previews.components + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import org.tasks.compose.components.SearchBar +import org.tasks.themes.TasksTheme + +@Preview(widthDp = 320, showBackground = true) +@Preview(widthDp = 320, showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun SearchBarPreviewPlaceholder() { + TasksTheme { + SearchBar( + text = "", + onTextChange = {}, + placeHolder = "Search", + onCloseClicked = {}, + onSearchClicked = {}, + ) + } +} + +@Preview(widthDp = 320, showBackground = true) +@Preview(widthDp = 320, showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun SearchBarPreviewSearching() { + TasksTheme { + SearchBar( + text = "Testing", + onTextChange = {}, + placeHolder = "Search", + onCloseClicked = {}, + onSearchClicked = {}, + ) + } +} diff --git a/kmp/src/androidMain/kotlin/org/tasks/previews/SearchableFilterPickerPreviews.kt b/kmp/src/androidMain/kotlin/org/tasks/previews/pickers/SearchableFilterPickerPreviews.kt similarity index 80% rename from kmp/src/androidMain/kotlin/org/tasks/previews/SearchableFilterPickerPreviews.kt rename to kmp/src/androidMain/kotlin/org/tasks/previews/pickers/SearchableFilterPickerPreviews.kt index 3f9da4bf7..19f10bd52 100644 --- a/kmp/src/androidMain/kotlin/org/tasks/previews/SearchableFilterPickerPreviews.kt +++ b/kmp/src/androidMain/kotlin/org/tasks/previews/pickers/SearchableFilterPickerPreviews.kt @@ -1,16 +1,13 @@ -package org.tasks.previews +package org.tasks.previews.pickers import android.content.res.Configuration -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AllInbox import androidx.compose.material.icons.outlined.QuestionMark -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import org.tasks.compose.pickers.SearchableFilterPicker +import org.tasks.themes.TasksTheme import org.tasks.filters.FilterImpl import org.tasks.filters.NavigationDrawerSubheader @@ -21,9 +18,7 @@ import org.tasks.filters.NavigationDrawerSubheader @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320) @Composable fun FilterPickerPreview() { - MaterialTheme( - colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() - ) { + TasksTheme { SearchableFilterPicker( filters = listOf( FilterImpl("My Tasks", icon = 4), diff --git a/kmp/src/commonMain/kotlin/org/tasks/compose/components/SearchBar.kt b/kmp/src/commonMain/kotlin/org/tasks/compose/components/SearchBar.kt new file mode 100644 index 000000000..0142d4ec4 --- /dev/null +++ b/kmp/src/commonMain/kotlin/org/tasks/compose/components/SearchBar.kt @@ -0,0 +1,79 @@ +package org.tasks.compose.components + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp + +@Composable +fun SearchBar( + modifier: Modifier = Modifier, + text: String, + onTextChange: (String) -> Unit, + placeHolder: String, + onCloseClicked: () -> Unit, + onSearchClicked: (String) -> Unit, +) { + OutlinedTextField( + shape = MaterialTheme.shapes.medium, + modifier = modifier.height(56.dp), + value = text, + onValueChange = { + onTextChange(it) + }, + placeholder = { + Text( + text = placeHolder, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxHeight(), + style = MaterialTheme.typography.bodyLarge, + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + trailingIcon = { + IconButton(onClick = { onCloseClicked() }) { + if (text.isNotBlank()) { + Icon( + imageVector = Icons.Outlined.Clear, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search + ), + keyboardActions = KeyboardActions( + onSearch = { + onSearchClicked(text) + } + ), + singleLine = true, + textStyle = MaterialTheme.typography.bodyLarge, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.onSurface, + unfocusedBorderColor = MaterialTheme.colorScheme.onSurface, + cursorColor = MaterialTheme.colorScheme.onSurface, + ), + ) +} diff --git a/app/src/main/java/org/tasks/themes/TasksTheme.kt b/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt similarity index 100% rename from app/src/main/java/org/tasks/themes/TasksTheme.kt rename to kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt