AppBar.kt 13 KB

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