Add search bar to drawer

pull/2919/head
Alex Baker 2 weeks ago
parent 28de989a05
commit 007c536312

@ -115,7 +115,7 @@ class MainActivity : AppCompatActivity() {
bottomPadding = WindowInsets.mandatorySystemGestures bottomPadding = WindowInsets.mandatorySystemGestures
.asPaddingValues() .asPaddingValues()
.calculateBottomPadding(), .calculateBottomPadding(),
items = state.drawerItems, items = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
begForMoney = state.begForMoney, begForMoney = state.begForMoney,
isTopAppBar = preferences.isTopAppBar, isTopAppBar = preferences.isTopAppBar,
setFilter = { viewModel.setFilter(it) }, setFilter = { viewModel.setFilter(it) },
@ -161,6 +161,8 @@ class MainActivity : AppCompatActivity() {
} }
}, },
dismiss = { viewModel.setDrawerOpen(false) }, dismiss = { viewModel.setDrawerOpen(false) },
query = state.menuQuery,
onQueryChange = { viewModel.queryMenu(it) },
) )
} }
} }

@ -62,6 +62,8 @@ class MainActivityViewModel @Inject constructor(
val task: Task? = null, val task: Task? = null,
val drawerOpen: Boolean = false, val drawerOpen: Boolean = false,
val drawerItems: ImmutableList<DrawerItem> = persistentListOf(), val drawerItems: ImmutableList<DrawerItem> = persistentListOf(),
val searchItems: ImmutableList<DrawerItem> = persistentListOf(),
val menuQuery: String = "",
) )
private val _state = MutableStateFlow( private val _state = MutableStateFlow(
@ -107,7 +109,12 @@ class MainActivityViewModel @Inject constructor(
} }
fun setDrawerOpen(open: Boolean) { fun setDrawerOpen(open: Boolean) {
_state.update { it.copy(drawerOpen = open) } _state.update {
it.copy(
drawerOpen = open,
menuQuery = if (!open) "" else it.menuQuery,
)
}
} }
init { init {
@ -152,6 +159,27 @@ class MainActivityViewModel @Inject constructor(
} }
} }
.let { filters -> _state.update { it.copy(drawerItems = filters.toPersistentList()) } } .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 { private fun getColor(filter: Filter): Int {
@ -202,4 +230,9 @@ class MainActivityViewModel @Inject constructor(
fun setTask(task: Task?) { fun setTask(task: Task?) {
_state.update { it.copy(task = task) } _state.update { it.copy(task = task) }
} }
fun queryMenu(query: String) {
_state.update { it.copy(menuQuery = query) }
updateFilters()
}
} }

@ -16,17 +16,18 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material.icons.outlined.PeopleOutline import androidx.compose.material.icons.outlined.PeopleOutline
import androidx.compose.material.icons.outlined.PermIdentity import androidx.compose.material.icons.outlined.PermIdentity
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SyncProblem import androidx.compose.material.icons.outlined.SyncProblem
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -34,7 +35,9 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
@ -49,6 +52,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import org.tasks.R import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.compose.components.SearchBar
import org.tasks.extensions.formatNumber import org.tasks.extensions.formatNumber
import org.tasks.filters.FilterImpl import org.tasks.filters.FilterImpl
import org.tasks.filters.NavigationDrawerSubheader import org.tasks.filters.NavigationDrawerSubheader
@ -63,7 +67,14 @@ fun TaskListDrawer(
onDrawerAction: (DrawerAction) -> Unit, onDrawerAction: (DrawerAction) -> Unit,
onAddClick: (DrawerItem.Header) -> Unit, onAddClick: (DrawerItem.Header) -> Unit,
onErrorClick: () -> Unit, onErrorClick: () -> Unit,
query: String,
onQueryChange: (String) -> Unit,
) { ) {
val searching by remember (query) {
derivedStateOf {
query.isNotBlank()
}
}
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.padding(bottom = bottomPadding) .padding(bottom = bottomPadding)
@ -73,7 +84,33 @@ fun TaskListDrawer(
stiffness = Spring.StiffnessMedium 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) { items(items = filters) {
when (it) { when (it) {
is DrawerItem.Filter -> FilterItem( is DrawerItem.Filter -> FilterItem(
@ -89,48 +126,42 @@ fun TaskListDrawer(
) )
} }
} }
item { if (!searching) {
Divider(modifier = Modifier.fillMaxWidth()) item {
} Divider(modifier = Modifier.fillMaxWidth())
if (begForMoney) { }
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 { item {
MenuAction( MenuAction(
icon = R.drawable.ic_outline_attach_money_24px, icon = R.drawable.ic_outline_edit_24px,
title = if (IS_GENERIC) R.string.TLA_menu_donate else R.string.name_your_price title = R.string.manage_drawer
) { ) {
onDrawerAction(DrawerAction.PURCHASE) onDrawerAction(DrawerAction.CUSTOMIZE_DRAWER)
} }
} }
} item {
item { MenuAction(
MenuAction( icon = R.drawable.ic_outline_help_outline_24px,
icon = R.drawable.ic_outline_edit_24px, title = R.string.help_and_feedback
title = R.string.manage_drawer ) {
) { onDrawerAction(DrawerAction.HELP_AND_FEEDBACK)
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)
} }
} }
} }
} }
@Composable @Composable
private fun FilterItem( internal fun FilterItem(
item: DrawerItem.Filter, item: DrawerItem.Filter,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -161,9 +192,7 @@ private fun FilterItem(
else -> Icons.Outlined.PeopleOutline else -> Icons.Outlined.PeopleOutline
}, },
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface.copy( tint = MaterialTheme.colorScheme.onSurface,
alpha = ContentAlpha.medium
),
) )
} }
Box( Box(
@ -207,12 +236,12 @@ private fun DrawerIcon(icon: Int, color: Int = 0) {
tint = when (color) { tint = when (color) {
0 -> MaterialTheme.colorScheme.onSurface 0 -> MaterialTheme.colorScheme.onSurface
else -> Color(color) else -> Color(color)
}.copy(alpha = ContentAlpha.medium) }
) )
} }
@Composable @Composable
private fun HeaderItem( internal fun HeaderItem(
item: DrawerItem.Header, item: DrawerItem.Header,
canAdd: Boolean, canAdd: Boolean,
toggleCollapsed: () -> Unit, toggleCollapsed: () -> Unit,
@ -289,6 +318,7 @@ private fun MenuRow(
fun MenuPreview() { fun MenuPreview() {
TasksTheme { TasksTheme {
TaskListDrawer( TaskListDrawer(
begForMoney = true,
filters = persistentListOf( filters = persistentListOf(
DrawerItem.Filter( DrawerItem.Filter(
title = "My Tasks", title = "My Tasks",
@ -313,9 +343,10 @@ fun MenuPreview() {
), ),
onClick = {}, onClick = {},
onDrawerAction = {}, onDrawerAction = {},
begForMoney = true,
onAddClick = {}, onAddClick = {},
onErrorClick = {}, onErrorClick = {},
query = "",
onQueryChange = {},
) )
} }
} }

@ -42,6 +42,8 @@ fun TasksMenu(
toggleCollapsed: (NavigationDrawerSubheader) -> Unit, toggleCollapsed: (NavigationDrawerSubheader) -> Unit,
addFilter: (NavigationDrawerSubheader) -> Unit, addFilter: (NavigationDrawerSubheader) -> Unit,
dismiss: () -> Unit, dismiss: () -> Unit,
query: String,
onQueryChange: (String) -> Unit,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val skipPartiallyExpanded = remember(expanded) { val skipPartiallyExpanded = remember(expanded) {
@ -128,6 +130,8 @@ fun TasksMenu(
onErrorClick = { onErrorClick = {
context.startActivity(Intent(context, MainPreferences::class.java)) context.startActivity(Intent(context, MainPreferences::class.java))
}, },
query = query,
onQueryChange = onQueryChange,
) )
} }
} }

@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp

@ -3,9 +3,7 @@ package org.tasks.widget
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.IntentCompat
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat

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

@ -1,16 +1,13 @@
package org.tasks.previews package org.tasks.previews.pickers
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AllInbox import androidx.compose.material.icons.outlined.AllInbox
import androidx.compose.material.icons.outlined.QuestionMark 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.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import org.tasks.compose.pickers.SearchableFilterPicker import org.tasks.compose.pickers.SearchableFilterPicker
import org.tasks.themes.TasksTheme
import org.tasks.filters.FilterImpl import org.tasks.filters.FilterImpl
import org.tasks.filters.NavigationDrawerSubheader import org.tasks.filters.NavigationDrawerSubheader
@ -21,9 +18,7 @@ import org.tasks.filters.NavigationDrawerSubheader
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable @Composable
fun FilterPickerPreview() { fun FilterPickerPreview() {
MaterialTheme( TasksTheme {
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
) {
SearchableFilterPicker( SearchableFilterPicker(
filters = listOf( filters = listOf(
FilterImpl("My Tasks", icon = 4), FilterImpl("My Tasks", icon = 4),

@ -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,
),
)
}
Loading…
Cancel
Save