TrackerSearch.kt 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. package eu.kanade.presentation.track
  2. import androidx.compose.animation.AnimatedVisibility
  3. import androidx.compose.animation.fadeIn
  4. import androidx.compose.animation.fadeOut
  5. import androidx.compose.animation.slideInVertically
  6. import androidx.compose.animation.slideOutVertically
  7. import androidx.compose.foundation.background
  8. import androidx.compose.foundation.border
  9. import androidx.compose.foundation.layout.Arrangement
  10. import androidx.compose.foundation.layout.Box
  11. import androidx.compose.foundation.layout.Column
  12. import androidx.compose.foundation.layout.PaddingValues
  13. import androidx.compose.foundation.layout.Row
  14. import androidx.compose.foundation.layout.Spacer
  15. import androidx.compose.foundation.layout.WindowInsets
  16. import androidx.compose.foundation.layout.fillMaxWidth
  17. import androidx.compose.foundation.layout.height
  18. import androidx.compose.foundation.layout.navigationBars
  19. import androidx.compose.foundation.layout.padding
  20. import androidx.compose.foundation.layout.paddingFromBaseline
  21. import androidx.compose.foundation.layout.width
  22. import androidx.compose.foundation.layout.windowInsetsPadding
  23. import androidx.compose.foundation.lazy.items
  24. import androidx.compose.foundation.selection.selectable
  25. import androidx.compose.foundation.shape.RoundedCornerShape
  26. import androidx.compose.foundation.text.BasicTextField
  27. import androidx.compose.foundation.text.KeyboardActions
  28. import androidx.compose.foundation.text.KeyboardOptions
  29. import androidx.compose.material.icons.Icons
  30. import androidx.compose.material.icons.automirrored.outlined.ArrowBack
  31. import androidx.compose.material.icons.filled.CheckCircle
  32. import androidx.compose.material.icons.filled.Close
  33. import androidx.compose.material3.Button
  34. import androidx.compose.material3.ButtonDefaults
  35. import androidx.compose.material3.HorizontalDivider
  36. import androidx.compose.material3.Icon
  37. import androidx.compose.material3.IconButton
  38. import androidx.compose.material3.MaterialTheme
  39. import androidx.compose.material3.Text
  40. import androidx.compose.material3.TopAppBar
  41. import androidx.compose.runtime.Composable
  42. import androidx.compose.runtime.remember
  43. import androidx.compose.ui.Alignment
  44. import androidx.compose.ui.Modifier
  45. import androidx.compose.ui.draw.clip
  46. import androidx.compose.ui.focus.FocusRequester
  47. import androidx.compose.ui.focus.focusRequester
  48. import androidx.compose.ui.graphics.Color
  49. import androidx.compose.ui.graphics.SolidColor
  50. import androidx.compose.ui.platform.LocalFocusManager
  51. import androidx.compose.ui.text.capitalize
  52. import androidx.compose.ui.text.input.ImeAction
  53. import androidx.compose.ui.text.input.TextFieldValue
  54. import androidx.compose.ui.text.intl.Locale
  55. import androidx.compose.ui.text.style.TextOverflow
  56. import androidx.compose.ui.text.toLowerCase
  57. import androidx.compose.ui.tooling.preview.PreviewLightDark
  58. import androidx.compose.ui.tooling.preview.PreviewParameter
  59. import androidx.compose.ui.unit.dp
  60. import eu.kanade.presentation.manga.components.MangaCover
  61. import eu.kanade.presentation.theme.TachiyomiTheme
  62. import eu.kanade.tachiyomi.data.track.model.TrackSearch
  63. import tachiyomi.i18n.MR
  64. import tachiyomi.presentation.core.components.ScrollbarLazyColumn
  65. import tachiyomi.presentation.core.components.material.Scaffold
  66. import tachiyomi.presentation.core.components.material.padding
  67. import tachiyomi.presentation.core.i18n.stringResource
  68. import tachiyomi.presentation.core.screens.EmptyScreen
  69. import tachiyomi.presentation.core.screens.LoadingScreen
  70. import tachiyomi.presentation.core.util.plus
  71. import tachiyomi.presentation.core.util.runOnEnterKeyPressed
  72. import tachiyomi.presentation.core.util.secondaryItemAlpha
  73. @Composable
  74. fun TrackerSearch(
  75. query: TextFieldValue,
  76. onQueryChange: (TextFieldValue) -> Unit,
  77. onDispatchQuery: () -> Unit,
  78. queryResult: Result<List<TrackSearch>>?,
  79. selected: TrackSearch?,
  80. onSelectedChange: (TrackSearch) -> Unit,
  81. onConfirmSelection: () -> Unit,
  82. onDismissRequest: () -> Unit,
  83. ) {
  84. val focusManager = LocalFocusManager.current
  85. val focusRequester = remember { FocusRequester() }
  86. val dispatchQueryAndClearFocus: () -> Unit = {
  87. onDispatchQuery()
  88. focusManager.clearFocus()
  89. }
  90. Scaffold(
  91. topBar = {
  92. Column {
  93. TopAppBar(
  94. navigationIcon = {
  95. IconButton(onClick = onDismissRequest) {
  96. Icon(
  97. imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
  98. contentDescription = null,
  99. tint = MaterialTheme.colorScheme.onSurfaceVariant,
  100. )
  101. }
  102. },
  103. title = {
  104. BasicTextField(
  105. value = query,
  106. onValueChange = onQueryChange,
  107. modifier = Modifier
  108. .fillMaxWidth()
  109. .focusRequester(focusRequester)
  110. .runOnEnterKeyPressed(action = dispatchQueryAndClearFocus),
  111. textStyle = MaterialTheme.typography.bodyLarge
  112. .copy(color = MaterialTheme.colorScheme.onSurface),
  113. singleLine = true,
  114. keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
  115. keyboardActions = KeyboardActions(onSearch = { dispatchQueryAndClearFocus() }),
  116. cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
  117. decorationBox = {
  118. if (query.text.isEmpty()) {
  119. Text(
  120. text = stringResource(MR.strings.action_search_hint),
  121. color = MaterialTheme.colorScheme.onSurfaceVariant,
  122. style = MaterialTheme.typography.bodyLarge,
  123. )
  124. }
  125. it()
  126. },
  127. )
  128. },
  129. actions = {
  130. if (query.text.isNotEmpty()) {
  131. IconButton(
  132. onClick = {
  133. onQueryChange(TextFieldValue())
  134. focusRequester.requestFocus()
  135. },
  136. ) {
  137. Icon(
  138. imageVector = Icons.Default.Close,
  139. contentDescription = null,
  140. tint = MaterialTheme.colorScheme.onSurfaceVariant,
  141. )
  142. }
  143. }
  144. },
  145. )
  146. HorizontalDivider()
  147. }
  148. },
  149. bottomBar = {
  150. AnimatedVisibility(
  151. visible = selected != null,
  152. enter = fadeIn() + slideInVertically { it / 2 },
  153. exit = slideOutVertically { it / 2 } + fadeOut(),
  154. ) {
  155. Button(
  156. onClick = { onConfirmSelection() },
  157. modifier = Modifier
  158. .padding(12.dp)
  159. .windowInsetsPadding(WindowInsets.navigationBars)
  160. .fillMaxWidth(),
  161. elevation = ButtonDefaults.elevatedButtonElevation(),
  162. ) {
  163. Text(text = stringResource(MR.strings.action_track))
  164. }
  165. }
  166. },
  167. ) { innerPadding ->
  168. if (queryResult == null) {
  169. LoadingScreen(modifier = Modifier.padding(innerPadding))
  170. } else {
  171. val availableTracks = queryResult.getOrNull()
  172. if (availableTracks != null) {
  173. if (availableTracks.isEmpty()) {
  174. EmptyScreen(
  175. modifier = Modifier.padding(innerPadding),
  176. stringRes = MR.strings.no_results_found,
  177. )
  178. } else {
  179. ScrollbarLazyColumn(
  180. contentPadding = innerPadding + PaddingValues(vertical = 12.dp),
  181. verticalArrangement = Arrangement.spacedBy(12.dp),
  182. ) {
  183. items(
  184. items = availableTracks,
  185. key = { it.hashCode() },
  186. ) {
  187. SearchResultItem(
  188. title = it.title,
  189. coverUrl = it.cover_url,
  190. type = it.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current),
  191. startDate = it.start_date,
  192. status = it.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current),
  193. score = it.score,
  194. description = it.summary.trim(),
  195. selected = it == selected,
  196. onClick = { onSelectedChange(it) },
  197. )
  198. }
  199. }
  200. }
  201. } else {
  202. EmptyScreen(
  203. modifier = Modifier.padding(innerPadding),
  204. message = queryResult.exceptionOrNull()?.message
  205. ?: stringResource(MR.strings.unknown_error),
  206. )
  207. }
  208. }
  209. }
  210. }
  211. @Composable
  212. private fun SearchResultItem(
  213. title: String,
  214. coverUrl: String,
  215. type: String,
  216. startDate: String,
  217. status: String,
  218. score: Float,
  219. description: String,
  220. selected: Boolean,
  221. onClick: () -> Unit,
  222. ) {
  223. val shape = RoundedCornerShape(16.dp)
  224. val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
  225. Box(
  226. modifier = Modifier
  227. .fillMaxWidth()
  228. .padding(horizontal = 12.dp)
  229. .clip(shape)
  230. .background(MaterialTheme.colorScheme.surface)
  231. .border(
  232. width = 2.dp,
  233. color = borderColor,
  234. shape = shape,
  235. )
  236. .selectable(selected = selected, onClick = onClick)
  237. .padding(12.dp),
  238. ) {
  239. if (selected) {
  240. Icon(
  241. imageVector = Icons.Filled.CheckCircle,
  242. contentDescription = null,
  243. modifier = Modifier.align(Alignment.TopEnd),
  244. tint = MaterialTheme.colorScheme.primary,
  245. )
  246. }
  247. Column {
  248. Row {
  249. MangaCover.Book(
  250. data = coverUrl,
  251. modifier = Modifier.height(96.dp),
  252. )
  253. Spacer(modifier = Modifier.width(12.dp))
  254. Column {
  255. Text(
  256. text = title,
  257. modifier = Modifier.padding(end = 28.dp),
  258. maxLines = 2,
  259. overflow = TextOverflow.Ellipsis,
  260. style = MaterialTheme.typography.titleMedium,
  261. )
  262. if (type.isNotBlank()) {
  263. SearchResultItemDetails(
  264. title = stringResource(MR.strings.track_type),
  265. text = type,
  266. )
  267. }
  268. if (startDate.isNotBlank()) {
  269. SearchResultItemDetails(
  270. title = stringResource(MR.strings.label_started),
  271. text = startDate,
  272. )
  273. }
  274. if (status.isNotBlank()) {
  275. SearchResultItemDetails(
  276. title = stringResource(MR.strings.track_status),
  277. text = status,
  278. )
  279. }
  280. if (score != -1f) {
  281. SearchResultItemDetails(
  282. title = stringResource(MR.strings.score),
  283. text = score.toString(),
  284. )
  285. }
  286. }
  287. }
  288. if (description.isNotBlank()) {
  289. Text(
  290. text = description,
  291. modifier = Modifier
  292. .paddingFromBaseline(top = 24.dp)
  293. .secondaryItemAlpha(),
  294. maxLines = 4,
  295. overflow = TextOverflow.Ellipsis,
  296. style = MaterialTheme.typography.bodySmall,
  297. )
  298. }
  299. }
  300. }
  301. }
  302. @Composable
  303. private fun SearchResultItemDetails(
  304. title: String,
  305. text: String,
  306. ) {
  307. Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
  308. Text(
  309. text = title,
  310. maxLines = 1,
  311. style = MaterialTheme.typography.titleSmall,
  312. )
  313. Text(
  314. text = text,
  315. modifier = Modifier
  316. .weight(1f)
  317. .secondaryItemAlpha(),
  318. maxLines = 1,
  319. overflow = TextOverflow.Ellipsis,
  320. style = MaterialTheme.typography.bodyMedium,
  321. )
  322. }
  323. }
  324. @PreviewLightDark
  325. @Composable
  326. private fun TrackerSearchPreviews(
  327. @PreviewParameter(TrackerSearchPreviewProvider::class)
  328. content: @Composable () -> Unit,
  329. ) {
  330. TachiyomiTheme { content() }
  331. }