AppBar.kt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. package eu.kanade.presentation.components
  2. import androidx.compose.foundation.basicMarquee
  3. import androidx.compose.foundation.interaction.MutableInteractionSource
  4. import androidx.compose.foundation.layout.Column
  5. import androidx.compose.foundation.layout.RowScope
  6. import androidx.compose.foundation.layout.fillMaxWidth
  7. import androidx.compose.foundation.text.BasicTextField
  8. import androidx.compose.foundation.text.KeyboardActions
  9. import androidx.compose.foundation.text.KeyboardOptions
  10. import androidx.compose.material.icons.Icons
  11. import androidx.compose.material.icons.automirrored.outlined.ArrowBack
  12. import androidx.compose.material.icons.outlined.Close
  13. import androidx.compose.material.icons.outlined.MoreVert
  14. import androidx.compose.material.icons.outlined.Search
  15. import androidx.compose.material3.DropdownMenuItem
  16. import androidx.compose.material3.Icon
  17. import androidx.compose.material3.IconButton
  18. import androidx.compose.material3.LocalContentColor
  19. import androidx.compose.material3.MaterialTheme
  20. import androidx.compose.material3.PlainTooltip
  21. import androidx.compose.material3.Text
  22. import androidx.compose.material3.TextFieldDefaults
  23. import androidx.compose.material3.TooltipBox
  24. import androidx.compose.material3.TooltipDefaults
  25. import androidx.compose.material3.TopAppBar
  26. import androidx.compose.material3.TopAppBarDefaults
  27. import androidx.compose.material3.TopAppBarScrollBehavior
  28. import androidx.compose.material3.rememberTooltipState
  29. import androidx.compose.material3.surfaceColorAtElevation
  30. import androidx.compose.runtime.Composable
  31. import androidx.compose.runtime.derivedStateOf
  32. import androidx.compose.runtime.getValue
  33. import androidx.compose.runtime.key
  34. import androidx.compose.runtime.mutableStateOf
  35. import androidx.compose.runtime.remember
  36. import androidx.compose.runtime.setValue
  37. import androidx.compose.ui.Modifier
  38. import androidx.compose.ui.focus.FocusRequester
  39. import androidx.compose.ui.focus.focusRequester
  40. import androidx.compose.ui.graphics.Color
  41. import androidx.compose.ui.graphics.SolidColor
  42. import androidx.compose.ui.graphics.vector.ImageVector
  43. import androidx.compose.ui.platform.LocalFocusManager
  44. import androidx.compose.ui.platform.LocalSoftwareKeyboardController
  45. import androidx.compose.ui.res.stringResource
  46. import androidx.compose.ui.text.font.FontWeight
  47. import androidx.compose.ui.text.input.ImeAction
  48. import androidx.compose.ui.text.input.VisualTransformation
  49. import androidx.compose.ui.text.style.TextOverflow
  50. import androidx.compose.ui.unit.dp
  51. import androidx.compose.ui.unit.sp
  52. import eu.kanade.tachiyomi.R
  53. import kotlinx.collections.immutable.ImmutableList
  54. import tachiyomi.i18n.MR
  55. import tachiyomi.presentation.core.i18n.localize
  56. import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
  57. import tachiyomi.presentation.core.util.runOnEnterKeyPressed
  58. import tachiyomi.presentation.core.util.secondaryItemAlpha
  59. import tachiyomi.presentation.core.util.showSoftKeyboard
  60. const val SEARCH_DEBOUNCE_MILLIS = 250L
  61. @Composable
  62. fun AppBar(
  63. modifier: Modifier = Modifier,
  64. backgroundColor: Color? = null,
  65. // Text
  66. title: String?,
  67. subtitle: String? = null,
  68. // Up button
  69. navigateUp: (() -> Unit)? = null,
  70. navigationIcon: ImageVector? = null,
  71. // Menu
  72. actions: @Composable RowScope.() -> Unit = {},
  73. // Action mode
  74. actionModeCounter: Int = 0,
  75. onCancelActionMode: () -> Unit = {},
  76. actionModeActions: @Composable RowScope.() -> Unit = {},
  77. scrollBehavior: TopAppBarScrollBehavior? = null,
  78. ) {
  79. val isActionMode by remember(actionModeCounter) {
  80. derivedStateOf { actionModeCounter > 0 }
  81. }
  82. AppBar(
  83. modifier = modifier,
  84. backgroundColor = backgroundColor,
  85. titleContent = {
  86. if (isActionMode) {
  87. AppBarTitle(actionModeCounter.toString())
  88. } else {
  89. AppBarTitle(title, subtitle)
  90. }
  91. },
  92. navigateUp = navigateUp,
  93. navigationIcon = navigationIcon,
  94. actions = {
  95. if (isActionMode) {
  96. actionModeActions()
  97. } else {
  98. actions()
  99. }
  100. },
  101. isActionMode = isActionMode,
  102. onCancelActionMode = onCancelActionMode,
  103. scrollBehavior = scrollBehavior,
  104. )
  105. }
  106. @Composable
  107. fun AppBar(
  108. modifier: Modifier = Modifier,
  109. backgroundColor: Color? = null,
  110. // Title
  111. titleContent: @Composable () -> Unit,
  112. // Up button
  113. navigateUp: (() -> Unit)? = null,
  114. navigationIcon: ImageVector? = null,
  115. // Menu
  116. actions: @Composable RowScope.() -> Unit = {},
  117. // Action mode
  118. isActionMode: Boolean = false,
  119. onCancelActionMode: () -> Unit = {},
  120. scrollBehavior: TopAppBarScrollBehavior? = null,
  121. ) {
  122. Column(
  123. modifier = modifier,
  124. ) {
  125. TopAppBar(
  126. navigationIcon = {
  127. if (isActionMode) {
  128. IconButton(onClick = onCancelActionMode) {
  129. Icon(
  130. imageVector = Icons.Outlined.Close,
  131. contentDescription = localize(MR.strings.action_cancel),
  132. )
  133. }
  134. } else {
  135. navigateUp?.let {
  136. IconButton(onClick = it) {
  137. UpIcon(navigationIcon)
  138. }
  139. }
  140. }
  141. },
  142. title = titleContent,
  143. actions = actions,
  144. colors = TopAppBarDefaults.topAppBarColors(
  145. containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
  146. elevation = if (isActionMode) 3.dp else 0.dp,
  147. ),
  148. ),
  149. scrollBehavior = scrollBehavior,
  150. )
  151. }
  152. }
  153. @Composable
  154. fun AppBarTitle(
  155. title: String?,
  156. subtitle: String? = null,
  157. ) {
  158. Column {
  159. title?.let {
  160. Text(
  161. text = it,
  162. maxLines = 1,
  163. overflow = TextOverflow.Ellipsis,
  164. )
  165. }
  166. subtitle?.let {
  167. Text(
  168. text = it,
  169. style = MaterialTheme.typography.bodyMedium,
  170. maxLines = 1,
  171. overflow = TextOverflow.Ellipsis,
  172. modifier = Modifier.basicMarquee(
  173. delayMillis = 2_000,
  174. ),
  175. )
  176. }
  177. }
  178. }
  179. @Composable
  180. fun AppBarActions(
  181. actions: ImmutableList<AppBar.AppBarAction>,
  182. ) {
  183. var showMenu by remember { mutableStateOf(false) }
  184. actions.filterIsInstance<AppBar.Action>().map {
  185. TooltipBox(
  186. positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
  187. tooltip = {
  188. PlainTooltip {
  189. Text(it.title)
  190. }
  191. },
  192. state = rememberTooltipState(),
  193. ) {
  194. IconButton(
  195. onClick = it.onClick,
  196. enabled = it.enabled,
  197. ) {
  198. Icon(
  199. imageVector = it.icon,
  200. tint = it.iconTint ?: LocalContentColor.current,
  201. contentDescription = it.title,
  202. )
  203. }
  204. }
  205. }
  206. val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
  207. if (overflowActions.isNotEmpty()) {
  208. TooltipBox(
  209. positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
  210. tooltip = {
  211. PlainTooltip {
  212. Text(stringResource(R.string.abc_action_menu_overflow_description))
  213. }
  214. },
  215. state = rememberTooltipState(),
  216. ) {
  217. IconButton(
  218. onClick = { showMenu = !showMenu },
  219. ) {
  220. Icon(
  221. Icons.Outlined.MoreVert,
  222. contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
  223. )
  224. }
  225. }
  226. DropdownMenu(
  227. expanded = showMenu,
  228. onDismissRequest = { showMenu = false },
  229. ) {
  230. overflowActions.map {
  231. DropdownMenuItem(
  232. onClick = {
  233. it.onClick()
  234. showMenu = false
  235. },
  236. text = { Text(it.title, fontWeight = FontWeight.Normal) },
  237. )
  238. }
  239. }
  240. }
  241. }
  242. /**
  243. * @param searchEnabled Set to false if you don't want to show search action.
  244. * @param searchQuery If null, use normal toolbar.
  245. * @param placeholderText If null, [MR.strings.action_search_hint] is used.
  246. */
  247. @Composable
  248. fun SearchToolbar(
  249. titleContent: @Composable () -> Unit = {},
  250. navigateUp: (() -> Unit)? = null,
  251. searchEnabled: Boolean = true,
  252. searchQuery: String?,
  253. onChangeSearchQuery: (String?) -> Unit,
  254. placeholderText: String? = null,
  255. onSearch: (String) -> Unit = {},
  256. onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
  257. actions: @Composable RowScope.() -> Unit = {},
  258. scrollBehavior: TopAppBarScrollBehavior? = null,
  259. visualTransformation: VisualTransformation = VisualTransformation.None,
  260. interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
  261. ) {
  262. val focusRequester = remember { FocusRequester() }
  263. AppBar(
  264. titleContent = {
  265. if (searchQuery == null) return@AppBar titleContent()
  266. val keyboardController = LocalSoftwareKeyboardController.current
  267. val focusManager = LocalFocusManager.current
  268. val searchAndClearFocus: () -> Unit = f@{
  269. if (searchQuery.isBlank()) return@f
  270. onSearch(searchQuery)
  271. focusManager.clearFocus()
  272. keyboardController?.hide()
  273. }
  274. BasicTextField(
  275. value = searchQuery,
  276. onValueChange = onChangeSearchQuery,
  277. modifier = Modifier
  278. .fillMaxWidth()
  279. .focusRequester(focusRequester)
  280. .runOnEnterKeyPressed(action = searchAndClearFocus)
  281. .showSoftKeyboard(remember { searchQuery.isEmpty() })
  282. .clearFocusOnSoftKeyboardHide(),
  283. textStyle = MaterialTheme.typography.titleMedium.copy(
  284. color = MaterialTheme.colorScheme.onBackground,
  285. fontWeight = FontWeight.Normal,
  286. fontSize = 18.sp,
  287. ),
  288. keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
  289. keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }),
  290. singleLine = true,
  291. cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
  292. visualTransformation = visualTransformation,
  293. interactionSource = interactionSource,
  294. decorationBox = { innerTextField ->
  295. TextFieldDefaults.TextFieldDecorationBox(
  296. value = searchQuery,
  297. innerTextField = innerTextField,
  298. enabled = true,
  299. singleLine = true,
  300. visualTransformation = visualTransformation,
  301. interactionSource = interactionSource,
  302. placeholder = {
  303. Text(
  304. modifier = Modifier.secondaryItemAlpha(),
  305. text = (placeholderText ?: localize(MR.strings.action_search_hint)),
  306. maxLines = 1,
  307. overflow = TextOverflow.Ellipsis,
  308. style = MaterialTheme.typography.titleMedium.copy(
  309. fontSize = 18.sp,
  310. fontWeight = FontWeight.Normal,
  311. ),
  312. )
  313. },
  314. )
  315. },
  316. )
  317. },
  318. navigateUp = if (searchQuery == null) navigateUp else onClickCloseSearch,
  319. actions = {
  320. key("search") {
  321. val onClick = { onChangeSearchQuery("") }
  322. if (!searchEnabled) {
  323. // Don't show search action
  324. } else if (searchQuery == null) {
  325. TooltipBox(
  326. positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
  327. tooltip = {
  328. PlainTooltip {
  329. Text(localize(MR.strings.action_search))
  330. }
  331. },
  332. state = rememberTooltipState(),
  333. ) {
  334. IconButton(
  335. onClick = onClick,
  336. ) {
  337. Icon(
  338. Icons.Outlined.Search,
  339. contentDescription = localize(MR.strings.action_search),
  340. )
  341. }
  342. }
  343. } else if (searchQuery.isNotEmpty()) {
  344. TooltipBox(
  345. positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
  346. tooltip = {
  347. PlainTooltip {
  348. Text(localize(MR.strings.action_reset))
  349. }
  350. },
  351. state = rememberTooltipState(),
  352. ) {
  353. IconButton(
  354. onClick = {
  355. onClick()
  356. focusRequester.requestFocus()
  357. },
  358. ) {
  359. Icon(
  360. Icons.Outlined.Close,
  361. contentDescription = localize(MR.strings.action_reset),
  362. )
  363. }
  364. }
  365. }
  366. }
  367. key("actions") { actions() }
  368. },
  369. isActionMode = false,
  370. scrollBehavior = scrollBehavior,
  371. )
  372. }
  373. @Composable
  374. fun UpIcon(navigationIcon: ImageVector? = null) {
  375. val icon = navigationIcon
  376. ?: Icons.AutoMirrored.Outlined.ArrowBack
  377. Icon(
  378. imageVector = icon,
  379. contentDescription = stringResource(R.string.abc_action_bar_up_description),
  380. )
  381. }
  382. sealed interface AppBar {
  383. sealed interface AppBarAction
  384. data class Action(
  385. val title: String,
  386. val icon: ImageVector,
  387. val iconTint: Color? = null,
  388. val onClick: () -> Unit,
  389. val enabled: Boolean = true,
  390. ) : AppBarAction
  391. data class OverflowAction(
  392. val title: String,
  393. val onClick: () -> Unit,
  394. ) : AppBarAction
  395. }