feat: add board detail create and filter toolbar UI

This commit is contained in:
2026-03-16 14:47:31 -04:00
parent de7bb48fe2
commit b936baf564
12 changed files with 596 additions and 16 deletions

View File

@@ -27,6 +27,7 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.color.MaterialColors
import com.google.android.material.chip.Chip
import java.text.DateFormat
import java.time.LocalDate
import java.util.ArrayDeque
import java.util.Date
import java.util.Locale
@@ -50,6 +51,7 @@ import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailDataSource
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardListDetail
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardTagSummary
import space.hackenslacker.kanbn4droid.app.boarddetail.CardBatchMutationResult
import space.hackenslacker.kanbn4droid.app.boarddetail.CreatedEntityRef
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
@@ -307,6 +309,54 @@ class BoardDetailFlowTest {
}
}
@Test
fun toolbarNormalMode_showsFilterAndSearchActions() {
val scenario = launchBoardDetail()
onView(withText("Card 1")).check(matches(isDisplayed()))
awaitCondition {
var present = false
scenario.onActivity { activity ->
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
present = menu.findItem(R.id.actionFilterByTag) != null && menu.findItem(R.id.actionSearch) != null
}
present
}
scenario.onActivity { activity ->
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
assertNotNull(menu.findItem(R.id.actionFilterByTag))
assertNotNull(menu.findItem(R.id.actionSearch))
}
}
@Test
fun selectionMode_replacesFilterSearchWithSelectionActions() {
val scenario = launchBoardDetail()
onView(withText("Card 1")).perform(longClick())
scenario.onActivity { activity ->
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
assertNotNull(menu.findItem(R.id.actionSelectAll))
assertNotNull(menu.findItem(R.id.actionMoveCards))
assertNotNull(menu.findItem(R.id.actionDeleteCards))
assertNull(menu.findItem(R.id.actionFilterByTag))
assertNull(menu.findItem(R.id.actionSearch))
}
}
@Test
fun fab_isVisible_andOpensAddChooser() {
launchBoardDetail()
onView(withId(R.id.boardDetailCreateFab)).check(matches(isDisplayed()))
onView(withId(R.id.boardDetailCreateFab)).perform(click())
onView(withText("Add new list")).inRoot(isDialog()).check(matches(isDisplayed()))
onView(withText("Add new card")).inRoot(isDialog()).check(matches(isDisplayed()))
}
@Test
fun missingBoardIdShowsBlockingDialogAndFinishes() {
val scenario = launchBoardDetail(boardId = null)
@@ -636,6 +686,20 @@ class BoardDetailFlowTest {
var lastDeleteCardIds: Set<String> = emptySet()
var lastMoveTargetListId: String? = null
override suspend fun createList(boardPublicId: String, title: String): BoardsApiResult<CreatedEntityRef> {
return BoardsApiResult.Failure("Not implemented in test fake")
}
override suspend fun createCard(
listPublicId: String,
title: String,
description: String?,
dueDate: LocalDate?,
tagPublicIds: Collection<String>,
): BoardsApiResult<CreatedEntityRef> {
return BoardsApiResult.Failure("Not implemented in test fake")
}
override suspend fun getBoardDetail(boardId: String): BoardsApiResult<BoardDetail> {
loadGate?.await()
return if (loadResults.isNotEmpty()) {

View File

@@ -2,20 +2,32 @@ package space.hackenslacker.kanbn4droid.app.boarddetail
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.CheckBox
import android.widget.LinearLayout
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import space.hackenslacker.kanbn4droid.app.MainActivity
@@ -42,8 +54,14 @@ class BoardDetailActivity : AppCompatActivity() {
private lateinit var fullScreenErrorContainer: View
private lateinit var fullScreenErrorText: TextView
private lateinit var retryButton: Button
private lateinit var createFab: FloatingActionButton
private var inlineTitleErrorMessage: String? = null
private var fabChooserDialog: AlertDialog? = null
private var addListDialog: AlertDialog? = null
private var addCardDialog: AlertDialog? = null
private var filterDialog: AlertDialog? = null
private var searchDialog: AlertDialog? = null
private var moveDialog: AlertDialog? = null
private var deleteSecondConfirmationDialog: AlertDialog? = null
private var dismissMoveDialogWhenMutationEnds: Boolean = false
@@ -105,6 +123,42 @@ class BoardDetailActivity : AppCompatActivity() {
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
renderToolbarMenu(menu, viewModel.uiState.value)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
renderToolbarMenu(menu, viewModel.uiState.value)
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.actionFilterByTag -> {
viewModel.onTagFilterIconTapped()
true
}
R.id.actionSearch -> {
viewModel.onSearchIconTapped()
true
}
R.id.actionSelectAll,
R.id.actionMoveCards,
R.id.actionDeleteCards,
-> handleSelectionAction(item)
else -> super.onOptionsItemSelected(item)
}
}
override fun onPostResume() {
super.onPostResume()
renderSelectionActions(viewModel.uiState.value)
}
private fun bindViews() {
toolbar = findViewById(R.id.boardDetailToolbar)
pager = findViewById(R.id.boardDetailPager)
@@ -113,6 +167,7 @@ class BoardDetailActivity : AppCompatActivity() {
fullScreenErrorContainer = findViewById(R.id.boardDetailFullScreenErrorContainer)
fullScreenErrorText = findViewById(R.id.boardDetailFullScreenErrorText)
retryButton = findViewById(R.id.boardDetailRetryButton)
createFab = findViewById(R.id.boardDetailCreateFab)
}
private fun setupToolbar() {
@@ -125,6 +180,9 @@ class BoardDetailActivity : AppCompatActivity() {
retryButton.setOnClickListener {
viewModel.retryLoad()
}
createFab.setOnClickListener {
viewModel.openFabChooser()
}
}
private fun setupPager() {
@@ -260,7 +318,10 @@ class BoardDetailActivity : AppCompatActivity() {
}
renderSelectionActions(state)
invalidateOptionsMenu()
renderOpenDialogs(state)
createFab.isEnabled = !state.isMutating
createFab.visibility = if (state.selectedCardIds.isEmpty()) View.VISIBLE else View.GONE
}
private fun showBlockingStartupErrorAndFinish(message: String) {
@@ -275,6 +336,69 @@ class BoardDetailActivity : AppCompatActivity() {
}
private fun renderOpenDialogs(state: BoardDetailUiState) {
if (state.isFabChooserOpen && fabChooserDialog == null) {
showFabChooserDialog(state)
} else if (!state.isFabChooserOpen) {
fabChooserDialog?.dismiss()
fabChooserDialog = null
}
if (state.isAddListDialogOpen && addListDialog == null) {
showAddListDialog(state)
}
addListDialog?.let { dialog ->
val titleLayout = dialog.findViewById<TextInputLayout>(R.id.addListTitleLayout)
val titleInput = dialog.findViewById<TextInputEditText>(R.id.addListTitleInput)
titleLayout?.error = state.addListTitleError
titleInput?.isEnabled = !state.isMutating
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = !state.isMutating
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.isEnabled = !state.isMutating
if (!state.isAddListDialogOpen) {
dialog.dismiss()
addListDialog = null
}
}
if (state.isAddCardDialogOpen && addCardDialog == null) {
showAddCardDialog(state)
}
addCardDialog?.let { dialog ->
val titleLayout = dialog.findViewById<TextInputLayout>(R.id.addCardTitleLayout)
val titleInput = dialog.findViewById<TextInputEditText>(R.id.addCardTitleInput)
val descriptionInput = dialog.findViewById<TextInputEditText>(R.id.addCardDescriptionInput)
titleLayout?.error = state.addCardTitleError
titleInput?.isEnabled = !state.isMutating
descriptionInput?.isEnabled = !state.isMutating
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = !state.isMutating
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.isEnabled = !state.isMutating
if (!state.isAddCardDialogOpen) {
dialog.dismiss()
addCardDialog = null
}
}
if (state.isFilterDialogOpen && filterDialog == null) {
showFilterDialog(state)
}
if (!state.isFilterDialogOpen) {
filterDialog?.dismiss()
filterDialog = null
}
if (state.isSearchDialogOpen && searchDialog == null) {
showSearchDialog(state)
}
searchDialog?.let { dialog ->
val input = dialog.findViewById<TextInputEditText>(R.id.searchTitleInput)
input?.isEnabled = !state.isMutating
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled = !state.isMutating
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.isEnabled = !state.isMutating
if (!state.isSearchDialogOpen) {
dialog.dismiss()
searchDialog = null
}
}
val activeMoveDialog = moveDialog
if (activeMoveDialog != null) {
val lists = state.boardDetail?.lists.orEmpty()
@@ -322,18 +446,43 @@ class BoardDetailActivity : AppCompatActivity() {
}
private fun renderSelectionActions(state: BoardDetailUiState) {
val inSelection = state.selectedCardIds.isNotEmpty()
toolbar.menu.clear()
if (!inSelection) {
// Kept for compatibility with existing call sites; menu rendering is delegated to
// framework options menu callbacks.
}
private fun renderToolbarMenu(menu: Menu, state: BoardDetailUiState) {
menu.clear()
if (state.selectedCardIds.isNotEmpty()) {
menu.add(Menu.NONE, R.id.actionSelectAll, Menu.NONE, getString(R.string.select_all)).apply {
setIcon(R.drawable.ic_select_all_grid_24)
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
tooltipText = getString(R.string.select_all)
}
menu.add(Menu.NONE, R.id.actionMoveCards, Menu.NONE, getString(R.string.move_cards)).apply {
setIcon(R.drawable.ic_move_cards_horizontal_24)
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
tooltipText = getString(R.string.move_cards)
}
menu.add(Menu.NONE, R.id.actionDeleteCards, Menu.NONE, getString(R.string.delete_cards)).apply {
setIcon(R.drawable.ic_delete_24)
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
tooltipText = getString(R.string.delete_cards)
}
return
}
toolbar.inflateMenu(R.menu.menu_board_detail_selection)
toolbar.menu.findItem(R.id.actionSelectAll)?.tooltipText = getString(R.string.select_all)
toolbar.menu.findItem(R.id.actionMoveCards)?.tooltipText = getString(R.string.move_cards)
toolbar.menu.findItem(R.id.actionDeleteCards)?.tooltipText = getString(R.string.delete_cards)
toolbar.setOnMenuItemClickListener { item ->
handleSelectionAction(item)
val filterItem = menu.add(Menu.NONE, R.id.actionFilterByTag, Menu.NONE, getString(R.string.filter_by_tag)).apply {
setIcon(R.drawable.ic_filter_list_24)
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
tooltipText = getString(R.string.filter_by_tag)
}
val searchItem = menu.add(Menu.NONE, R.id.actionSearch, Menu.NONE, getString(R.string.search)).apply {
setIcon(R.drawable.ic_search_24)
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
tooltipText = getString(R.string.search)
}
tintMainMenuIcon(filterItem, state.activeTagFilterIds.isNotEmpty())
tintMainMenuIcon(searchItem, state.activeTitleQuery.isNotBlank())
}
private fun handleSelectionAction(item: MenuItem): Boolean {
@@ -357,6 +506,189 @@ class BoardDetailActivity : AppCompatActivity() {
}
}
private fun tintMainMenuIcon(item: MenuItem?, isActive: Boolean) {
val drawableRes = when (item?.itemId) {
R.id.actionFilterByTag -> R.drawable.ic_filter_list_24
R.id.actionSearch -> R.drawable.ic_search_24
else -> null
} ?: return
val icon = AppCompatResources.getDrawable(this, drawableRes)?.mutate() ?: return
val wrapped = DrawableCompat.wrap(icon)
val tintColor = if (isActive) {
MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimary, ContextCompat.getColor(this, android.R.color.holo_blue_light))
} else {
MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant, ContextCompat.getColor(this, android.R.color.black))
}
DrawableCompat.setTint(wrapped, tintColor)
item?.icon = wrapped
}
private fun showFabChooserDialog(state: BoardDetailUiState) {
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(48, 24, 48, 24)
}
val addListButton = MaterialButton(this).apply {
text = getString(R.string.add_new_list)
setOnClickListener {
viewModel.openAddListDialog()
}
}
val addCardButton = MaterialButton(this).apply {
text = getString(R.string.add_new_card)
isEnabled = state.canAddCard
setOnClickListener {
viewModel.openAddCardDialog()
}
}
val helperText = TextView(this).apply {
text = getString(R.string.create_a_list_first_to_add_cards)
visibility = if (state.canAddCard) View.GONE else View.VISIBLE
setPadding(8, 4, 8, 0)
}
root.addView(addListButton)
root.addView(addCardButton)
root.addView(helperText)
val dialog = MaterialAlertDialogBuilder(this)
.setView(root)
.setOnDismissListener {
if (fabChooserDialog != null) {
viewModel.closeFabChooser()
}
fabChooserDialog = null
}
.create()
fabChooserDialog = dialog
dialog.show()
}
private fun showAddListDialog(state: BoardDetailUiState) {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_list, null)
val titleLayout: TextInputLayout = dialogView.findViewById(R.id.addListTitleLayout)
val titleInput: TextInputEditText = dialogView.findViewById(R.id.addListTitleInput)
titleInput.setText(state.addListTitleDraft)
titleInput.doAfterTextChanged { viewModel.updateAddListTitle(it?.toString().orEmpty()) }
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.add_new_list)
.setView(dialogView)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.add_list, null)
.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
titleLayout.error = null
viewModel.createList()
}
}
dialog.setOnDismissListener {
if (addListDialog != null && viewModel.uiState.value.isAddListDialogOpen) {
viewModel.cancelAddListDialog()
}
addListDialog = null
}
addListDialog = dialog
dialog.show()
}
private fun showAddCardDialog(state: BoardDetailUiState) {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_card, null)
val titleInput: TextInputEditText = dialogView.findViewById(R.id.addCardTitleInput)
val descriptionInput: TextInputEditText = dialogView.findViewById(R.id.addCardDescriptionInput)
titleInput.setText(state.addCardTitleDraft)
descriptionInput.setText(state.addCardDescriptionDraft)
titleInput.doAfterTextChanged { viewModel.updateAddCardTitle(it?.toString().orEmpty()) }
descriptionInput.doAfterTextChanged { viewModel.updateAddCardDescription(it?.toString().orEmpty()) }
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.add_new_card)
.setView(dialogView)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.add_card, null)
.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
viewModel.createCard()
}
}
dialog.setOnDismissListener {
if (addCardDialog != null && viewModel.uiState.value.isAddCardDialogOpen) {
viewModel.cancelAddCardDialog()
}
addCardDialog = null
}
addCardDialog = dialog
dialog.show()
}
private fun showFilterDialog(state: BoardDetailUiState) {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_filter_tags, null)
val container: LinearLayout = dialogView.findViewById(R.id.filterTagsContainer)
val tags = state.boardDetail
?.lists
.orEmpty()
.flatMap { it.cards }
.flatMap { it.tags }
.distinctBy { it.id }
.sortedBy { it.name.lowercase() }
tags.forEach { tag ->
val checkBox = CheckBox(this).apply {
text = tag.name
isChecked = state.pendingTagFilterIds.contains(tag.id)
setOnCheckedChangeListener { _, _ ->
val selected = mutableSetOf<String>()
for (i in 0 until container.childCount) {
val child = container.getChildAt(i)
if (child is CheckBox && child.isChecked) {
selected.add(tags[i].id)
}
}
viewModel.updatePendingTagFilterIds(selected)
}
}
container.addView(checkBox)
}
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.filter_by_tag)
.setView(dialogView)
.setNegativeButton(R.string.cancel) { _, _ -> viewModel.cancelFilterDialog() }
.setPositiveButton(R.string.filter) { _, _ -> viewModel.applyFilterDialog() }
.create()
dialog.setOnDismissListener {
if (filterDialog != null && viewModel.uiState.value.isFilterDialogOpen) {
viewModel.cancelFilterDialog()
}
filterDialog = null
}
filterDialog = dialog
dialog.show()
}
private fun showSearchDialog(state: BoardDetailUiState) {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_search_title, null)
val input: TextInputEditText = dialogView.findViewById(R.id.searchTitleInput)
input.setText(state.pendingTitleQuery)
input.doAfterTextChanged { viewModel.updatePendingTitleQuery(it?.toString().orEmpty()) }
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.search_by_title)
.setView(dialogView)
.setNegativeButton(R.string.cancel) { _, _ -> viewModel.cancelSearchDialog() }
.setPositiveButton(R.string.search) { _, _ -> viewModel.applySearchDialog() }
.create()
dialog.setOnDismissListener {
if (searchDialog != null && viewModel.uiState.value.isSearchDialogOpen) {
viewModel.cancelSearchDialog()
}
searchDialog = null
}
searchDialog = dialog
dialog.show()
}
private fun showMoveCardsDialog() {
val lists = viewModel.uiState.value.boardDetail?.lists.orEmpty()
if (lists.isEmpty()) {

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M4,7h16v2H4zM7,11h10v2H7zM10,15h4v2h-4z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27a6,6 0,1 0,-1.41,1.41l0.27,0.28v0.79L20,21.5L21.5,20zM10,14a4,4 0,1 1,0 -8a4,4 0,0 1,0 8z" />
</vector>

View File

@@ -68,4 +68,13 @@
android:text="@string/retry" />
</LinearLayout>
</FrameLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/boardDetailCreateFab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/board_detail_add"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:paddingEnd="24dp"
android:paddingBottom="8dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/addCardTitleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/card_title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/addCardTitleInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences|text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/description">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/addCardDescriptionInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top|start"
android:inputType="textMultiLine|textCapSentences"
android:minLines="3" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:paddingEnd="24dp"
android:paddingBottom="8dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/addListTitleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/list_title_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/addListTitleInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences|text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:paddingEnd="24dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/filterTagsPlaceholderText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/filter_tags_placeholder"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
<LinearLayout
android:id="@+id/filterTagsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:paddingEnd="24dp"
android:paddingBottom="8dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/searchTitleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/searchTitleInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/actionFilterByTag"
android:icon="@drawable/ic_filter_list_24"
android:title="@string/filter_by_tag"
app:showAsAction="always" />
<item
android:id="@+id/actionSearch"
android:icon="@drawable/ic_search_24"
android:title="@string/search"
app:showAsAction="always" />
</menu>

View File

@@ -50,4 +50,18 @@
<string name="card_detail_placeholder_subtitle">Card detail view is coming soon.</string>
<string name="board_detail_unable_to_open_board">Unable to open board.</string>
<string name="board_detail_session_expired">Session expired. Please sign in again.</string>
<string name="board_detail_add">Add</string>
<string name="filter_by_tag">Filter by tag</string>
<string name="search">Search</string>
<string name="search_by_title">Search by title</string>
<string name="filter">Filter</string>
<string name="add_new_list">Add new list</string>
<string name="add_new_card">Add new card</string>
<string name="add_list">Add list</string>
<string name="add_card">Add card</string>
<string name="create_a_list_first_to_add_cards">Create a list first to add cards.</string>
<string name="card_title">Card title</string>
<string name="card_title_required">Card title is required</string>
<string name="description">Description</string>
<string name="filter_tags_placeholder">Tag selector will be wired in the next task.</string>
</resources>