package eu.kanade.presentation.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue 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.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R import kotlinx.coroutines.delay @Composable fun AppBar( modifier: Modifier = Modifier, // Text title: String?, subtitle: String? = null, // Up button navigateUp: (() -> Unit)? = null, navigationIcon: ImageVector = Icons.Default.ArrowBack, // Menu actions: @Composable RowScope.() -> Unit = {}, // Action mode actionModeCounter: Int = 0, onCancelActionMode: () -> Unit = {}, actionModeActions: @Composable RowScope.() -> Unit = {}, // Banners downloadedOnlyMode: Boolean = false, incognitoMode: Boolean = false, scrollBehavior: TopAppBarScrollBehavior? = null, ) { val isActionMode by derivedStateOf { actionModeCounter > 0 } AppBar( modifier = modifier, titleContent = { if (isActionMode) { AppBarTitle(actionModeCounter.toString()) } else { AppBarTitle(title, subtitle) } }, navigateUp = navigateUp, navigationIcon = navigationIcon, actions = { if (isActionMode) { actionModeActions() } else { actions() } }, isActionMode = isActionMode, onCancelActionMode = onCancelActionMode, downloadedOnlyMode = downloadedOnlyMode, incognitoMode = incognitoMode, scrollBehavior = scrollBehavior, ) } @Composable fun AppBar( modifier: Modifier = Modifier, // Title titleContent: @Composable () -> Unit, // Up button navigateUp: (() -> Unit)? = null, navigationIcon: ImageVector = Icons.Default.ArrowBack, // Menu actions: @Composable RowScope.() -> Unit = {}, // Action mode isActionMode: Boolean = false, onCancelActionMode: () -> Unit = {}, // Banners downloadedOnlyMode: Boolean = false, incognitoMode: Boolean = false, scrollBehavior: TopAppBarScrollBehavior? = null, ) { Column( modifier = modifier, ) { SmallTopAppBar( navigationIcon = { if (isActionMode) { IconButton(onClick = onCancelActionMode) { Icon( imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.action_cancel), ) } } else { navigateUp?.let { IconButton(onClick = it) { Icon( imageVector = navigationIcon, contentDescription = stringResource(R.string.abc_action_bar_up_description), ) } } } }, title = titleContent, actions = actions, windowInsets = WindowInsets.statusBars, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( elevation = if (isActionMode) 3.dp else 0.dp, ), ), scrollBehavior = scrollBehavior, ) if (downloadedOnlyMode) { DownloadedOnlyModeBanner() } if (incognitoMode) { IncognitoModeBanner() } } } @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, ) } } } @Composable fun AppBarActions( actions: List, ) { var showMenu by remember { mutableStateOf(false) } actions.filterIsInstance().map { IconButton( onClick = it.onClick, enabled = it.enabled, ) { Icon( imageVector = it.icon, contentDescription = it.title, ) } } val overflowActions = actions.filterIsInstance() if (overflowActions.isNotEmpty()) { IconButton(onClick = { showMenu = !showMenu }) { Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.label_more)) } DropdownMenu( expanded = showMenu, onDismissRequest = { showMenu = false }, ) { overflowActions.map { DropdownMenuItem( onClick = { it.onClick() showMenu = false }, text = { Text(it.title, fontWeight = FontWeight.Normal) }, ) } } } } @Composable fun SearchToolbar( searchQuery: String, onChangeSearchQuery: (String) -> Unit, onClickCloseSearch: () -> Unit, onClickResetSearch: () -> Unit, incognitoMode: Boolean = false, downloadedOnlyMode: Boolean = false, scrollBehavior: TopAppBarScrollBehavior? = null, ) { val focusRequester = remember { FocusRequester.Default } AppBar( titleContent = { BasicTextField( value = searchQuery, onValueChange = onChangeSearchQuery, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester), textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground), singleLine = true, cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), ) }, navigationIcon = Icons.Outlined.ArrowBack, navigateUp = onClickCloseSearch, actions = { AnimatedVisibility(visible = searchQuery.isNotEmpty()) { IconButton(onClick = onClickResetSearch) { Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset)) } } }, isActionMode = false, downloadedOnlyMode = downloadedOnlyMode, incognitoMode = incognitoMode, scrollBehavior = scrollBehavior, ) LaunchedEffect(focusRequester) { // TODO: https://issuetracker.google.com/issues/204502668 delay(100) focusRequester.requestFocus() } } sealed interface AppBar { sealed interface AppBarAction data class Action( val title: String, val icon: ImageVector, val onClick: () -> Unit, val enabled: Boolean = true, ) : AppBarAction data class OverflowAction( val title: String, val onClick: () -> Unit, ) : AppBarAction }