tachiyomi/app/src/main/java/eu/kanade/presentation/components/AppBar.kt

426 lines
15 KiB
Kotlin
Raw Normal View History

2022-04-24 16:22:22 +02:00
package eu.kanade.presentation.components
2023-10-14 18:30:17 +02:00
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.interaction.MutableInteractionSource
2022-04-24 16:22:22 +02:00
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.TextFieldDefaults
2022-04-24 16:22:22 +02:00
import androidx.compose.material.icons.Icons
2023-11-16 04:19:02 +01:00
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
2022-04-24 16:22:22 +02:00
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
2023-05-21 04:46:16 +02:00
import androidx.compose.material3.LocalContentColor
2022-04-24 16:22:22 +02:00
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
2022-04-24 16:22:22 +02:00
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
2022-09-22 04:30:06 +02:00
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
2022-04-24 16:22:22 +02:00
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
2022-04-24 16:22:22 +02:00
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
2022-04-24 16:22:22 +02:00
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
2023-05-21 04:46:16 +02:00
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
2022-04-24 16:22:22 +02:00
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
2022-04-24 16:22:22 +02:00
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
2023-11-17 15:46:13 +01:00
import kotlinx.collections.immutable.ImmutableList
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha
import tachiyomi.presentation.core.util.showSoftKeyboard
2022-04-24 16:22:22 +02:00
const val SEARCH_DEBOUNCE_MILLIS = 250L
@Composable
fun AppBar(
title: String?,
modifier: Modifier = Modifier,
2023-10-14 18:30:17 +02:00
backgroundColor: Color? = null,
// Text
subtitle: String? = null,
// Up button
navigateUp: (() -> Unit)? = null,
2023-07-08 01:58:53 +02:00
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,
2023-10-14 18:30:17 +02:00
backgroundColor = backgroundColor,
titleContent = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
} else {
AppBarTitle(title, subtitle = subtitle)
}
},
navigateUp = navigateUp,
navigationIcon = navigationIcon,
actions = {
if (isActionMode) {
actionModeActions()
} else {
actions()
}
},
isActionMode = isActionMode,
onCancelActionMode = onCancelActionMode,
scrollBehavior = scrollBehavior,
)
}
@Composable
fun AppBar(
// Title
titleContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Up button
navigateUp: (() -> Unit)? = null,
2023-07-08 01:58:53 +02:00
navigationIcon: ImageVector? = null,
// Menu
actions: @Composable RowScope.() -> Unit = {},
// Action mode
isActionMode: Boolean = false,
onCancelActionMode: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
Column(
modifier = modifier,
) {
2022-09-22 04:30:06 +02:00
TopAppBar(
navigationIcon = {
if (isActionMode) {
IconButton(onClick = onCancelActionMode) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(MR.strings.action_cancel),
)
}
} else {
navigateUp?.let {
IconButton(onClick = it) {
UpIcon(navigationIcon = navigationIcon)
}
}
}
},
title = titleContent,
actions = actions,
2023-02-23 05:09:16 +01:00
colors = TopAppBarDefaults.topAppBarColors(
2023-10-14 18:30:17 +02:00
containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = if (isActionMode) 3.dp else 0.dp,
),
),
scrollBehavior = scrollBehavior,
)
}
}
2022-04-24 16:22:22 +02:00
@Composable
fun AppBarTitle(
title: String?,
modifier: Modifier = Modifier,
2022-04-24 16:22:22 +02:00
subtitle: String? = null,
) {
Column(modifier = modifier) {
2022-04-24 16:22:22 +02:00
title?.let {
Text(
text = it,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
subtitle?.let {
Text(
text = it,
2022-04-24 20:39:51 +02:00
style = MaterialTheme.typography.bodyMedium,
2022-04-24 16:22:22 +02:00
maxLines = 1,
overflow = TextOverflow.Ellipsis,
2023-10-14 18:30:17 +02:00
modifier = Modifier.basicMarquee(
delayMillis = 2_000,
),
2022-04-24 16:22:22 +02:00
)
}
}
}
@Composable
fun AppBarActions(
2023-11-17 15:46:13 +01:00
actions: ImmutableList<AppBar.AppBarAction>,
2022-04-24 16:22:22 +02:00
) {
var showMenu by remember { mutableStateOf(false) }
actions.filterIsInstance<AppBar.Action>().map {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(it.title)
}
},
state = rememberTooltipState(),
2022-04-24 16:22:22 +02:00
) {
IconButton(
onClick = it.onClick,
enabled = it.enabled,
) {
Icon(
imageVector = it.icon,
2023-05-21 04:46:16 +02:00
tint = it.iconTint ?: LocalContentColor.current,
contentDescription = it.title,
)
}
2022-04-24 16:22:22 +02:00
}
}
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
if (overflowActions.isNotEmpty()) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_menu_overflow_description))
}
},
state = rememberTooltipState(),
) {
IconButton(
onClick = { showMenu = !showMenu },
) {
Icon(
Icons.Outlined.MoreVert,
contentDescription = stringResource(MR.strings.action_menu_overflow_description),
)
}
2022-04-24 16:22:22 +02:00
}
DropdownMenu(
expanded = showMenu,
2022-04-24 20:39:51 +02:00
onDismissRequest = { showMenu = false },
2022-04-24 16:22:22 +02:00
) {
overflowActions.map {
DropdownMenuItem(
onClick = {
it.onClick()
showMenu = false
},
text = { Text(it.title, fontWeight = FontWeight.Normal) },
2022-04-24 16:22:22 +02:00
)
}
}
}
}
/**
* @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(
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier,
titleContent: @Composable () -> Unit = {},
navigateUp: (() -> Unit)? = null,
searchEnabled: Boolean = true,
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(
modifier = modifier,
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 = {
2023-02-18 23:04:32 +01:00
Text(
modifier = Modifier.secondaryItemAlpha(),
text = (placeholderText ?: stringResource(MR.strings.action_search_hint)),
2023-02-18 23:04:32 +01:00
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(stringResource(MR.strings.action_search))
}
},
state = rememberTooltipState(),
) {
IconButton(
onClick = onClick,
) {
Icon(
Icons.Outlined.Search,
contentDescription = stringResource(MR.strings.action_search),
)
}
}
} else if (searchQuery.isNotEmpty()) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_reset))
}
},
state = rememberTooltipState(),
) {
IconButton(
onClick = {
onClick()
focusRequester.requestFocus()
},
) {
Icon(
Icons.Outlined.Close,
contentDescription = stringResource(MR.strings.action_reset),
)
}
}
}
}
key("actions") { actions() }
},
isActionMode = false,
scrollBehavior = scrollBehavior,
)
}
2023-07-08 01:58:53 +02:00
@Composable
fun UpIcon(
modifier: Modifier = Modifier,
navigationIcon: ImageVector? = null,
) {
2023-07-08 01:58:53 +02:00
val icon = navigationIcon
2023-11-16 04:19:02 +01:00
?: Icons.AutoMirrored.Outlined.ArrowBack
2023-07-08 01:58:53 +02:00
Icon(
imageVector = icon,
contentDescription = stringResource(MR.strings.action_bar_up_description),
modifier = modifier,
2023-07-08 01:58:53 +02:00
)
}
sealed interface AppBar {
sealed interface AppBarAction
2022-04-24 16:22:22 +02:00
data class Action(
val title: String,
val icon: ImageVector,
2023-05-21 04:46:16 +02:00
val iconTint: Color? = null,
2022-04-24 16:22:22 +02:00
val onClick: () -> Unit,
2022-04-24 20:39:51 +02:00
val enabled: Boolean = true,
2022-04-24 16:22:22 +02:00
) : AppBarAction
data class OverflowAction(
val title: String,
val onClick: () -> Unit,
) : AppBarAction
}