123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- package eu.kanade.presentation.components
- import androidx.compose.foundation.basicMarquee
- import androidx.compose.foundation.interaction.MutableInteractionSource
- import androidx.compose.foundation.layout.Column
- import androidx.compose.foundation.layout.RowScope
- import androidx.compose.foundation.layout.fillMaxWidth
- import androidx.compose.foundation.text.BasicTextField
- import androidx.compose.foundation.text.KeyboardActions
- import androidx.compose.foundation.text.KeyboardOptions
- import androidx.compose.material.icons.Icons
- import androidx.compose.material.icons.automirrored.outlined.ArrowBack
- import androidx.compose.material.icons.outlined.Close
- import androidx.compose.material.icons.outlined.MoreVert
- import androidx.compose.material.icons.outlined.Search
- import androidx.compose.material3.DropdownMenuItem
- import androidx.compose.material3.Icon
- import androidx.compose.material3.IconButton
- import androidx.compose.material3.LocalContentColor
- import androidx.compose.material3.MaterialTheme
- import androidx.compose.material3.PlainTooltip
- import androidx.compose.material3.Text
- import androidx.compose.material3.TextFieldDefaults
- import androidx.compose.material3.TooltipBox
- import androidx.compose.material3.TooltipDefaults
- import androidx.compose.material3.TopAppBar
- import androidx.compose.material3.TopAppBarDefaults
- import androidx.compose.material3.TopAppBarScrollBehavior
- import androidx.compose.material3.rememberTooltipState
- import androidx.compose.material3.surfaceColorAtElevation
- import androidx.compose.runtime.Composable
- import androidx.compose.runtime.derivedStateOf
- import androidx.compose.runtime.getValue
- import androidx.compose.runtime.key
- import androidx.compose.runtime.mutableStateOf
- import androidx.compose.runtime.remember
- import androidx.compose.runtime.setValue
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.focus.FocusRequester
- import androidx.compose.ui.focus.focusRequester
- import androidx.compose.ui.graphics.Color
- import androidx.compose.ui.graphics.SolidColor
- import androidx.compose.ui.graphics.vector.ImageVector
- import androidx.compose.ui.platform.LocalFocusManager
- import androidx.compose.ui.platform.LocalSoftwareKeyboardController
- import androidx.compose.ui.res.stringResource
- import androidx.compose.ui.text.font.FontWeight
- import androidx.compose.ui.text.input.ImeAction
- import androidx.compose.ui.text.input.VisualTransformation
- import androidx.compose.ui.text.style.TextOverflow
- import androidx.compose.ui.unit.dp
- import androidx.compose.ui.unit.sp
- import eu.kanade.tachiyomi.R
- import kotlinx.collections.immutable.ImmutableList
- import tachiyomi.i18n.MR
- import tachiyomi.presentation.core.i18n.localize
- import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
- import tachiyomi.presentation.core.util.runOnEnterKeyPressed
- import tachiyomi.presentation.core.util.secondaryItemAlpha
- import tachiyomi.presentation.core.util.showSoftKeyboard
- const val SEARCH_DEBOUNCE_MILLIS = 250L
- @Composable
- fun AppBar(
- modifier: Modifier = Modifier,
- backgroundColor: Color? = null,
- // Text
- title: String?,
- subtitle: String? = null,
- // Up button
- navigateUp: (() -> Unit)? = null,
- navigationIcon: ImageVector? = null,
- // Menu
- actions: @Composable RowScope.() -> Unit = {},
- // Action mode
- actionModeCounter: Int = 0,
- onCancelActionMode: () -> Unit = {},
- actionModeActions: @Composable RowScope.() -> Unit = {},
- scrollBehavior: TopAppBarScrollBehavior? = null,
- ) {
- val isActionMode by remember(actionModeCounter) {
- derivedStateOf { actionModeCounter > 0 }
- }
- AppBar(
- modifier = modifier,
- backgroundColor = backgroundColor,
- titleContent = {
- if (isActionMode) {
- AppBarTitle(actionModeCounter.toString())
- } else {
- AppBarTitle(title, subtitle)
- }
- },
- navigateUp = navigateUp,
- navigationIcon = navigationIcon,
- actions = {
- if (isActionMode) {
- actionModeActions()
- } else {
- actions()
- }
- },
- isActionMode = isActionMode,
- onCancelActionMode = onCancelActionMode,
- scrollBehavior = scrollBehavior,
- )
- }
- @Composable
- fun AppBar(
- modifier: Modifier = Modifier,
- backgroundColor: Color? = null,
- // Title
- titleContent: @Composable () -> Unit,
- // Up button
- navigateUp: (() -> Unit)? = null,
- navigationIcon: ImageVector? = null,
- // Menu
- actions: @Composable RowScope.() -> Unit = {},
- // Action mode
- isActionMode: Boolean = false,
- onCancelActionMode: () -> Unit = {},
- scrollBehavior: TopAppBarScrollBehavior? = null,
- ) {
- Column(
- modifier = modifier,
- ) {
- TopAppBar(
- navigationIcon = {
- if (isActionMode) {
- IconButton(onClick = onCancelActionMode) {
- Icon(
- imageVector = Icons.Outlined.Close,
- contentDescription = localize(MR.strings.action_cancel),
- )
- }
- } else {
- navigateUp?.let {
- IconButton(onClick = it) {
- UpIcon(navigationIcon)
- }
- }
- }
- },
- title = titleContent,
- actions = actions,
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
- elevation = if (isActionMode) 3.dp else 0.dp,
- ),
- ),
- scrollBehavior = scrollBehavior,
- )
- }
- }
- @Composable
- fun AppBarTitle(
- title: String?,
- subtitle: String? = null,
- ) {
- Column {
- title?.let {
- Text(
- text = it,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
- subtitle?.let {
- Text(
- text = it,
- style = MaterialTheme.typography.bodyMedium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.basicMarquee(
- delayMillis = 2_000,
- ),
- )
- }
- }
- }
- @Composable
- fun AppBarActions(
- actions: ImmutableList<AppBar.AppBarAction>,
- ) {
- var showMenu by remember { mutableStateOf(false) }
- actions.filterIsInstance<AppBar.Action>().map {
- TooltipBox(
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
- tooltip = {
- PlainTooltip {
- Text(it.title)
- }
- },
- state = rememberTooltipState(),
- ) {
- IconButton(
- onClick = it.onClick,
- enabled = it.enabled,
- ) {
- Icon(
- imageVector = it.icon,
- tint = it.iconTint ?: LocalContentColor.current,
- contentDescription = it.title,
- )
- }
- }
- }
- val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
- if (overflowActions.isNotEmpty()) {
- TooltipBox(
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
- tooltip = {
- PlainTooltip {
- Text(stringResource(R.string.abc_action_menu_overflow_description))
- }
- },
- state = rememberTooltipState(),
- ) {
- IconButton(
- onClick = { showMenu = !showMenu },
- ) {
- Icon(
- Icons.Outlined.MoreVert,
- contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
- )
- }
- }
- DropdownMenu(
- expanded = showMenu,
- onDismissRequest = { showMenu = false },
- ) {
- overflowActions.map {
- DropdownMenuItem(
- onClick = {
- it.onClick()
- showMenu = false
- },
- text = { Text(it.title, fontWeight = FontWeight.Normal) },
- )
- }
- }
- }
- }
- /**
- * @param searchEnabled Set to false if you don't want to show search action.
- * @param searchQuery If null, use normal toolbar.
- * @param placeholderText If null, [MR.strings.action_search_hint] is used.
- */
- @Composable
- fun SearchToolbar(
- titleContent: @Composable () -> Unit = {},
- navigateUp: (() -> Unit)? = null,
- searchEnabled: Boolean = true,
- searchQuery: String?,
- onChangeSearchQuery: (String?) -> Unit,
- placeholderText: String? = null,
- onSearch: (String) -> Unit = {},
- onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
- actions: @Composable RowScope.() -> Unit = {},
- scrollBehavior: TopAppBarScrollBehavior? = null,
- visualTransformation: VisualTransformation = VisualTransformation.None,
- interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
- ) {
- val focusRequester = remember { FocusRequester() }
- AppBar(
- titleContent = {
- if (searchQuery == null) return@AppBar titleContent()
- val keyboardController = LocalSoftwareKeyboardController.current
- val focusManager = LocalFocusManager.current
- val searchAndClearFocus: () -> Unit = f@{
- if (searchQuery.isBlank()) return@f
- onSearch(searchQuery)
- focusManager.clearFocus()
- keyboardController?.hide()
- }
- BasicTextField(
- value = searchQuery,
- onValueChange = onChangeSearchQuery,
- modifier = Modifier
- .fillMaxWidth()
- .focusRequester(focusRequester)
- .runOnEnterKeyPressed(action = searchAndClearFocus)
- .showSoftKeyboard(remember { searchQuery.isEmpty() })
- .clearFocusOnSoftKeyboardHide(),
- textStyle = MaterialTheme.typography.titleMedium.copy(
- color = MaterialTheme.colorScheme.onBackground,
- fontWeight = FontWeight.Normal,
- fontSize = 18.sp,
- ),
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
- keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }),
- singleLine = true,
- cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
- visualTransformation = visualTransformation,
- interactionSource = interactionSource,
- decorationBox = { innerTextField ->
- TextFieldDefaults.TextFieldDecorationBox(
- value = searchQuery,
- innerTextField = innerTextField,
- enabled = true,
- singleLine = true,
- visualTransformation = visualTransformation,
- interactionSource = interactionSource,
- placeholder = {
- Text(
- modifier = Modifier.secondaryItemAlpha(),
- text = (placeholderText ?: localize(MR.strings.action_search_hint)),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.titleMedium.copy(
- fontSize = 18.sp,
- fontWeight = FontWeight.Normal,
- ),
- )
- },
- )
- },
- )
- },
- navigateUp = if (searchQuery == null) navigateUp else onClickCloseSearch,
- actions = {
- key("search") {
- val onClick = { onChangeSearchQuery("") }
- if (!searchEnabled) {
- // Don't show search action
- } else if (searchQuery == null) {
- TooltipBox(
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
- tooltip = {
- PlainTooltip {
- Text(localize(MR.strings.action_search))
- }
- },
- state = rememberTooltipState(),
- ) {
- IconButton(
- onClick = onClick,
- ) {
- Icon(
- Icons.Outlined.Search,
- contentDescription = localize(MR.strings.action_search),
- )
- }
- }
- } else if (searchQuery.isNotEmpty()) {
- TooltipBox(
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
- tooltip = {
- PlainTooltip {
- Text(localize(MR.strings.action_reset))
- }
- },
- state = rememberTooltipState(),
- ) {
- IconButton(
- onClick = {
- onClick()
- focusRequester.requestFocus()
- },
- ) {
- Icon(
- Icons.Outlined.Close,
- contentDescription = localize(MR.strings.action_reset),
- )
- }
- }
- }
- }
- key("actions") { actions() }
- },
- isActionMode = false,
- scrollBehavior = scrollBehavior,
- )
- }
- @Composable
- fun UpIcon(navigationIcon: ImageVector? = null) {
- val icon = navigationIcon
- ?: Icons.AutoMirrored.Outlined.ArrowBack
- Icon(
- imageVector = icon,
- contentDescription = stringResource(R.string.abc_action_bar_up_description),
- )
- }
- sealed interface AppBar {
- sealed interface AppBarAction
- data class Action(
- val title: String,
- val icon: ImageVector,
- val iconTint: Color? = null,
- val onClick: () -> Unit,
- val enabled: Boolean = true,
- ) : AppBarAction
- data class OverflowAction(
- val title: String,
- val onClick: () -> Unit,
- ) : AppBarAction
- }
|