Compare commits
2 Commits
7ab8fcb5f5
...
11adfd66bc
| Author | SHA1 | Date | |
|---|---|---|---|
| 11adfd66bc | |||
| bfe493e5c9 |
@@ -5,12 +5,15 @@ import android.content.pm.PackageManager
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -31,6 +34,7 @@ import androidx.compose.material.icons.filled.SelectAll
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -43,6 +47,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -67,6 +72,7 @@ import de.jeanlucmakiola.calendula.R
|
|||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.domain.RecurringWriteScope
|
import de.jeanlucmakiola.calendula.domain.RecurringWriteScope
|
||||||
import de.jeanlucmakiola.floret.identity.animateItemMotion
|
import de.jeanlucmakiola.floret.identity.animateItemMotion
|
||||||
|
import de.jeanlucmakiola.floret.identity.fadeThrough
|
||||||
import de.jeanlucmakiola.floret.identity.predictiveBack
|
import de.jeanlucmakiola.floret.identity.predictiveBack
|
||||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||||
@@ -160,51 +166,18 @@ fun SearchScreen(
|
|||||||
modifier = modifier.predictiveBack(onBack = onBack, enabled = !inSelection),
|
modifier = modifier.predictiveBack(onBack = onBack, enabled = !inSelection),
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
topBar = {
|
topBar = {
|
||||||
if (inSelection) {
|
SearchTopBar(
|
||||||
SelectionTopBar(
|
inSelection = inSelection,
|
||||||
count = selection.size,
|
query = query,
|
||||||
onClose = viewModel::clearSelection,
|
selectedCount = selection.size,
|
||||||
onSelectAll = viewModel::selectAll,
|
focusRequester = focusRequester,
|
||||||
onDelete = { requireWrite { showDeleteDialog = true } },
|
onQueryChange = viewModel::setQuery,
|
||||||
)
|
onImeAction = { keyboard?.hide() },
|
||||||
} else {
|
onBack = onBack,
|
||||||
TopAppBar(
|
onCloseSelection = viewModel::clearSelection,
|
||||||
title = {
|
onSelectAll = viewModel::selectAll,
|
||||||
InlineTextField(
|
onDelete = { requireWrite { showDeleteDialog = true } },
|
||||||
value = query,
|
)
|
||||||
onValueChange = viewModel::setQuery,
|
|
||||||
placeholder = stringResource(R.string.search_hint),
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
|
||||||
imeAction = ImeAction.Search,
|
|
||||||
onImeAction = { keyboard?.hide() },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.search_back),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
if (query.isNotEmpty()) {
|
|
||||||
IconButton(onClick = { viewModel.setQuery("") }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Close,
|
|
||||||
contentDescription = stringResource(R.string.search_clear),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
// imePadding shrinks the content by the keyboard, so the centered
|
// imePadding shrinks the content by the keyboard, so the centered
|
||||||
@@ -284,43 +257,130 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Contextual bar replacing the search field while results are picked. */
|
/**
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
* The search field and the contextual selection bar as one bar: entering or
|
||||||
|
* leaving selection mode keeps the bar itself in place, eases its container
|
||||||
|
* colour and fades its slots through, rather than cutting between two bars.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectionTopBar(
|
private fun SearchTopBar(
|
||||||
count: Int,
|
inSelection: Boolean,
|
||||||
onClose: () -> Unit,
|
query: String,
|
||||||
|
selectedCount: Int,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
onImeAction: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onCloseSelection: () -> Unit,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val container by animateColorAsState(
|
||||||
|
targetValue = if (inSelection) {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surface
|
||||||
|
},
|
||||||
|
animationSpec = MaterialTheme.motionScheme.fastEffectsSpec(),
|
||||||
|
label = "search-top-bar-color",
|
||||||
|
)
|
||||||
|
// Held past the selection emptying so the count has something to draw while
|
||||||
|
// it fades out — by then the set is already at zero.
|
||||||
|
var lastCount by remember { mutableIntStateOf(0) }
|
||||||
|
if (selectedCount > 0) lastCount = selectedCount
|
||||||
|
|
||||||
|
// Read here so the non-composable transitionSpec lambdas can capture it.
|
||||||
|
val slotSwap = fadeThrough()
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(pluralStringResource(R.plurals.search_selected_count, count, count)) },
|
title = {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = inSelection,
|
||||||
|
transitionSpec = { slotSwap },
|
||||||
|
label = "search-top-bar-title",
|
||||||
|
) { selecting ->
|
||||||
|
if (selecting) {
|
||||||
|
Text(
|
||||||
|
pluralStringResource(
|
||||||
|
R.plurals.search_selected_count,
|
||||||
|
lastCount,
|
||||||
|
lastCount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
InlineTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = onQueryChange,
|
||||||
|
placeholder = stringResource(R.string.search_hint),
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
imeAction = ImeAction.Search,
|
||||||
|
onImeAction = onImeAction,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onClose) {
|
AnimatedContent(
|
||||||
Icon(
|
targetState = inSelection,
|
||||||
imageVector = Icons.Default.Close,
|
transitionSpec = { slotSwap },
|
||||||
contentDescription = stringResource(R.string.search_selection_close),
|
label = "search-top-bar-navigation",
|
||||||
)
|
) { selecting ->
|
||||||
|
if (selecting) {
|
||||||
|
IconButton(onClick = onCloseSelection) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.search_selection_close),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.search_back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onSelectAll) {
|
AnimatedContent(
|
||||||
Icon(
|
targetState = inSelection,
|
||||||
imageVector = Icons.Default.SelectAll,
|
transitionSpec = { slotSwap },
|
||||||
contentDescription = stringResource(R.string.search_select_all),
|
label = "search-top-bar-actions",
|
||||||
)
|
) { selecting ->
|
||||||
}
|
Row {
|
||||||
IconButton(onClick = onDelete) {
|
if (selecting) {
|
||||||
Icon(
|
IconButton(onClick = onSelectAll) {
|
||||||
imageVector = Icons.Default.Delete,
|
Icon(
|
||||||
contentDescription = stringResource(R.string.search_delete_selected),
|
imageVector = Icons.Default.SelectAll,
|
||||||
tint = MaterialTheme.colorScheme.error,
|
contentDescription = stringResource(R.string.search_select_all),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onDelete) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
R.string.search_delete_selected,
|
||||||
|
),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (query.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { onQueryChange("") }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.search_clear),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = container),
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +395,9 @@ private fun SearchResults(
|
|||||||
val events = results.events
|
val events = results.events
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 96.dp),
|
// No horizontal inset here: GroupedRow already insets itself, and adding
|
||||||
|
// another 16dp made the results narrower than every other list.
|
||||||
|
contentPadding = PaddingValues(top = 8.dp, bottom = 96.dp),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = events,
|
items = events,
|
||||||
|
|||||||
Reference in New Issue
Block a user