MangaInfoHeader.kt 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. package eu.kanade.presentation.manga.components
  2. import android.content.Context
  3. import androidx.compose.animation.animateContentSize
  4. import androidx.compose.animation.core.animateFloatAsState
  5. import androidx.compose.animation.graphics.res.animatedVectorResource
  6. import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
  7. import androidx.compose.animation.graphics.vector.AnimatedImageVector
  8. import androidx.compose.foundation.background
  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.RowScope
  15. import androidx.compose.foundation.layout.Spacer
  16. import androidx.compose.foundation.layout.fillMaxWidth
  17. import androidx.compose.foundation.layout.height
  18. import androidx.compose.foundation.layout.padding
  19. import androidx.compose.foundation.layout.size
  20. import androidx.compose.foundation.layout.sizeIn
  21. import androidx.compose.foundation.lazy.LazyRow
  22. import androidx.compose.foundation.lazy.items
  23. import androidx.compose.foundation.text.selection.SelectionContainer
  24. import androidx.compose.material.icons.Icons
  25. import androidx.compose.material.icons.filled.Favorite
  26. import androidx.compose.material.icons.filled.Warning
  27. import androidx.compose.material.icons.outlined.AttachMoney
  28. import androidx.compose.material.icons.outlined.Block
  29. import androidx.compose.material.icons.outlined.Close
  30. import androidx.compose.material.icons.outlined.Done
  31. import androidx.compose.material.icons.outlined.DoneAll
  32. import androidx.compose.material.icons.outlined.FavoriteBorder
  33. import androidx.compose.material.icons.outlined.Pause
  34. import androidx.compose.material.icons.outlined.Public
  35. import androidx.compose.material.icons.outlined.Schedule
  36. import androidx.compose.material.icons.outlined.Sync
  37. import androidx.compose.material3.DropdownMenuItem
  38. import androidx.compose.material3.Icon
  39. import androidx.compose.material3.LocalContentColor
  40. import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
  41. import androidx.compose.material3.MaterialTheme
  42. import androidx.compose.material3.ProvideTextStyle
  43. import androidx.compose.material3.SuggestionChip
  44. import androidx.compose.material3.SuggestionChipDefaults
  45. import androidx.compose.material3.Text
  46. import androidx.compose.runtime.Composable
  47. import androidx.compose.runtime.CompositionLocalProvider
  48. import androidx.compose.runtime.getValue
  49. import androidx.compose.runtime.mutableStateOf
  50. import androidx.compose.runtime.remember
  51. import androidx.compose.runtime.saveable.rememberSaveable
  52. import androidx.compose.runtime.setValue
  53. import androidx.compose.ui.Alignment
  54. import androidx.compose.ui.Modifier
  55. import androidx.compose.ui.draw.alpha
  56. import androidx.compose.ui.draw.clipToBounds
  57. import androidx.compose.ui.draw.drawWithContent
  58. import androidx.compose.ui.graphics.Brush
  59. import androidx.compose.ui.graphics.Color
  60. import androidx.compose.ui.graphics.vector.ImageVector
  61. import androidx.compose.ui.layout.ContentScale
  62. import androidx.compose.ui.layout.SubcomposeLayout
  63. import androidx.compose.ui.platform.LocalContext
  64. import androidx.compose.ui.platform.LocalDensity
  65. import androidx.compose.ui.res.pluralStringResource
  66. import androidx.compose.ui.res.stringResource
  67. import androidx.compose.ui.text.style.TextAlign
  68. import androidx.compose.ui.text.style.TextOverflow
  69. import androidx.compose.ui.unit.Constraints
  70. import androidx.compose.ui.unit.Dp
  71. import androidx.compose.ui.unit.dp
  72. import androidx.compose.ui.unit.sp
  73. import coil.compose.AsyncImage
  74. import com.google.accompanist.flowlayout.FlowRow
  75. import eu.kanade.presentation.components.DropdownMenu
  76. import eu.kanade.presentation.components.MangaCover
  77. import eu.kanade.presentation.components.TextButton
  78. import eu.kanade.presentation.util.clickableNoIndication
  79. import eu.kanade.presentation.util.padding
  80. import eu.kanade.presentation.util.secondaryItemAlpha
  81. import eu.kanade.tachiyomi.R
  82. import eu.kanade.tachiyomi.source.model.SManga
  83. import eu.kanade.tachiyomi.util.system.copyToClipboard
  84. import tachiyomi.domain.manga.model.Manga
  85. import kotlin.math.roundToInt
  86. private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
  87. @Composable
  88. fun MangaInfoBox(
  89. modifier: Modifier = Modifier,
  90. isTabletUi: Boolean,
  91. appBarPadding: Dp,
  92. title: String,
  93. author: String?,
  94. artist: String?,
  95. sourceName: String,
  96. isStubSource: Boolean,
  97. coverDataProvider: () -> Manga,
  98. status: Long,
  99. onCoverClick: () -> Unit,
  100. doSearch: (query: String, global: Boolean) -> Unit,
  101. ) {
  102. Box(modifier = modifier) {
  103. // Backdrop
  104. val backdropGradientColors = listOf(
  105. Color.Transparent,
  106. MaterialTheme.colorScheme.background,
  107. )
  108. AsyncImage(
  109. model = coverDataProvider(),
  110. contentDescription = null,
  111. contentScale = ContentScale.Crop,
  112. modifier = Modifier
  113. .matchParentSize()
  114. .drawWithContent {
  115. drawContent()
  116. drawRect(
  117. brush = Brush.verticalGradient(colors = backdropGradientColors),
  118. )
  119. }
  120. .alpha(.2f),
  121. )
  122. // Manga & source info
  123. CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
  124. if (!isTabletUi) {
  125. MangaAndSourceTitlesSmall(
  126. appBarPadding = appBarPadding,
  127. coverDataProvider = coverDataProvider,
  128. onCoverClick = onCoverClick,
  129. title = title,
  130. context = LocalContext.current,
  131. doSearch = doSearch,
  132. author = author,
  133. artist = artist,
  134. status = status,
  135. sourceName = sourceName,
  136. isStubSource = isStubSource,
  137. )
  138. } else {
  139. MangaAndSourceTitlesLarge(
  140. appBarPadding = appBarPadding,
  141. coverDataProvider = coverDataProvider,
  142. onCoverClick = onCoverClick,
  143. title = title,
  144. context = LocalContext.current,
  145. doSearch = doSearch,
  146. author = author,
  147. artist = artist,
  148. status = status,
  149. sourceName = sourceName,
  150. isStubSource = isStubSource,
  151. )
  152. }
  153. }
  154. }
  155. }
  156. @Composable
  157. fun MangaActionRow(
  158. modifier: Modifier = Modifier,
  159. favorite: Boolean,
  160. trackingCount: Int,
  161. onAddToLibraryClicked: () -> Unit,
  162. onWebViewClicked: (() -> Unit)?,
  163. onWebViewLongClicked: (() -> Unit)?,
  164. onTrackingClicked: (() -> Unit)?,
  165. onEditCategory: (() -> Unit)?,
  166. ) {
  167. Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
  168. val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
  169. MangaActionButton(
  170. title = if (favorite) {
  171. stringResource(R.string.in_library)
  172. } else {
  173. stringResource(R.string.add_to_library)
  174. },
  175. icon = if (favorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
  176. color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
  177. onClick = onAddToLibraryClicked,
  178. onLongClick = onEditCategory,
  179. )
  180. if (onTrackingClicked != null) {
  181. MangaActionButton(
  182. title = if (trackingCount == 0) {
  183. stringResource(R.string.manga_tracking_tab)
  184. } else {
  185. pluralStringResource(id = R.plurals.num_trackers, count = trackingCount, trackingCount)
  186. },
  187. icon = if (trackingCount == 0) Icons.Outlined.Sync else Icons.Outlined.Done,
  188. color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
  189. onClick = onTrackingClicked,
  190. )
  191. }
  192. if (onWebViewClicked != null) {
  193. MangaActionButton(
  194. title = stringResource(R.string.action_web_view),
  195. icon = Icons.Outlined.Public,
  196. color = defaultActionButtonColor,
  197. onClick = onWebViewClicked,
  198. onLongClick = onWebViewLongClicked,
  199. )
  200. }
  201. }
  202. }
  203. @Composable
  204. fun ExpandableMangaDescription(
  205. modifier: Modifier = Modifier,
  206. defaultExpandState: Boolean,
  207. description: String?,
  208. tagsProvider: () -> List<String>?,
  209. onTagSearch: (String) -> Unit,
  210. onCopyTagToClipboard: (tag: String) -> Unit,
  211. ) {
  212. Column(modifier = modifier) {
  213. val (expanded, onExpanded) = rememberSaveable {
  214. mutableStateOf(defaultExpandState)
  215. }
  216. val desc =
  217. description.takeIf { !it.isNullOrBlank() } ?: stringResource(R.string.description_placeholder)
  218. val trimmedDescription = remember(desc) {
  219. desc
  220. .replace(whitespaceLineRegex, "\n")
  221. .trimEnd()
  222. }
  223. MangaSummary(
  224. expandedDescription = desc,
  225. shrunkDescription = trimmedDescription,
  226. expanded = expanded,
  227. modifier = Modifier
  228. .padding(top = 8.dp)
  229. .padding(horizontal = 16.dp)
  230. .clickableNoIndication { onExpanded(!expanded) },
  231. )
  232. val tags = tagsProvider()
  233. if (!tags.isNullOrEmpty()) {
  234. Box(
  235. modifier = Modifier
  236. .padding(top = 8.dp)
  237. .padding(vertical = 12.dp)
  238. .animateContentSize(),
  239. ) {
  240. var showMenu by remember { mutableStateOf(false) }
  241. var tagSelected by remember { mutableStateOf("") }
  242. DropdownMenu(
  243. expanded = showMenu,
  244. onDismissRequest = { showMenu = false },
  245. ) {
  246. DropdownMenuItem(
  247. text = { Text(text = stringResource(R.string.action_search)) },
  248. onClick = {
  249. onTagSearch(tagSelected)
  250. showMenu = false
  251. },
  252. )
  253. DropdownMenuItem(
  254. text = { Text(text = stringResource(R.string.action_copy_to_clipboard)) },
  255. onClick = {
  256. onCopyTagToClipboard(tagSelected)
  257. showMenu = false
  258. },
  259. )
  260. }
  261. if (expanded) {
  262. FlowRow(
  263. modifier = Modifier.padding(horizontal = 16.dp),
  264. mainAxisSpacing = 4.dp,
  265. crossAxisSpacing = 8.dp,
  266. ) {
  267. tags.forEach {
  268. TagsChip(
  269. text = it,
  270. onClick = {
  271. tagSelected = it
  272. showMenu = true
  273. },
  274. )
  275. }
  276. }
  277. } else {
  278. LazyRow(
  279. contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
  280. horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
  281. ) {
  282. items(items = tags) {
  283. TagsChip(
  284. text = it,
  285. onClick = {
  286. tagSelected = it
  287. showMenu = true
  288. },
  289. )
  290. }
  291. }
  292. }
  293. }
  294. }
  295. }
  296. }
  297. @Composable
  298. private fun MangaAndSourceTitlesLarge(
  299. appBarPadding: Dp,
  300. coverDataProvider: () -> Manga,
  301. onCoverClick: () -> Unit,
  302. title: String,
  303. context: Context,
  304. doSearch: (query: String, global: Boolean) -> Unit,
  305. author: String?,
  306. artist: String?,
  307. status: Long,
  308. sourceName: String,
  309. isStubSource: Boolean,
  310. ) {
  311. Column(
  312. modifier = Modifier
  313. .fillMaxWidth()
  314. .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
  315. horizontalAlignment = Alignment.CenterHorizontally,
  316. ) {
  317. MangaCover.Book(
  318. modifier = Modifier.fillMaxWidth(0.65f),
  319. data = coverDataProvider(),
  320. contentDescription = stringResource(R.string.manga_cover),
  321. onClick = onCoverClick,
  322. )
  323. Spacer(modifier = Modifier.height(16.dp))
  324. Text(
  325. text = title.ifBlank { stringResource(R.string.unknown_title) },
  326. style = MaterialTheme.typography.titleLarge,
  327. modifier = Modifier.clickableNoIndication(
  328. onLongClick = { if (title.isNotBlank()) context.copyToClipboard(title, title) },
  329. onClick = { if (title.isNotBlank()) doSearch(title, true) },
  330. ),
  331. textAlign = TextAlign.Center,
  332. )
  333. Spacer(modifier = Modifier.height(2.dp))
  334. Text(
  335. text = author?.takeIf { it.isNotBlank() } ?: stringResource(R.string.unknown_author),
  336. style = MaterialTheme.typography.titleSmall,
  337. modifier = Modifier
  338. .secondaryItemAlpha()
  339. .padding(top = 2.dp)
  340. .clickableNoIndication(
  341. onLongClick = {
  342. if (!author.isNullOrBlank()) {
  343. context.copyToClipboard(
  344. author,
  345. author,
  346. )
  347. }
  348. },
  349. onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
  350. ),
  351. textAlign = TextAlign.Center,
  352. )
  353. if (!artist.isNullOrBlank() && author != artist) {
  354. Text(
  355. text = artist,
  356. style = MaterialTheme.typography.titleSmall,
  357. modifier = Modifier
  358. .secondaryItemAlpha()
  359. .padding(top = 2.dp)
  360. .clickableNoIndication(
  361. onLongClick = { context.copyToClipboard(artist, artist) },
  362. onClick = { doSearch(artist, true) },
  363. ),
  364. textAlign = TextAlign.Center,
  365. )
  366. }
  367. Spacer(modifier = Modifier.height(4.dp))
  368. Row(
  369. modifier = Modifier.secondaryItemAlpha(),
  370. verticalAlignment = Alignment.CenterVertically,
  371. ) {
  372. Icon(
  373. imageVector = when (status) {
  374. SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
  375. SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
  376. SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
  377. SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
  378. SManga.CANCELLED.toLong() -> Icons.Outlined.Close
  379. SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
  380. else -> Icons.Outlined.Block
  381. },
  382. contentDescription = null,
  383. modifier = Modifier
  384. .padding(end = 4.dp)
  385. .size(16.dp),
  386. )
  387. ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
  388. Text(
  389. text = when (status) {
  390. SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
  391. SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
  392. SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
  393. SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
  394. SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
  395. SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
  396. else -> stringResource(R.string.unknown)
  397. },
  398. overflow = TextOverflow.Ellipsis,
  399. maxLines = 1,
  400. )
  401. DotSeparatorText()
  402. if (isStubSource) {
  403. Icon(
  404. imageVector = Icons.Filled.Warning,
  405. contentDescription = null,
  406. modifier = Modifier
  407. .padding(end = 4.dp)
  408. .size(16.dp),
  409. tint = MaterialTheme.colorScheme.error,
  410. )
  411. }
  412. Text(
  413. text = sourceName,
  414. modifier = Modifier.clickableNoIndication { doSearch(sourceName, false) },
  415. overflow = TextOverflow.Ellipsis,
  416. maxLines = 1,
  417. )
  418. }
  419. }
  420. }
  421. }
  422. @Composable
  423. private fun MangaAndSourceTitlesSmall(
  424. appBarPadding: Dp,
  425. coverDataProvider: () -> Manga,
  426. onCoverClick: () -> Unit,
  427. title: String,
  428. context: Context,
  429. doSearch: (query: String, global: Boolean) -> Unit,
  430. author: String?,
  431. artist: String?,
  432. status: Long,
  433. sourceName: String,
  434. isStubSource: Boolean,
  435. ) {
  436. Row(
  437. modifier = Modifier
  438. .fillMaxWidth()
  439. .padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
  440. verticalAlignment = Alignment.CenterVertically,
  441. ) {
  442. MangaCover.Book(
  443. modifier = Modifier
  444. .sizeIn(maxWidth = 100.dp)
  445. .align(Alignment.Top),
  446. data = coverDataProvider(),
  447. contentDescription = stringResource(R.string.manga_cover),
  448. onClick = onCoverClick,
  449. )
  450. Column(modifier = Modifier.padding(start = 16.dp)) {
  451. Text(
  452. text = title.ifBlank { stringResource(R.string.unknown_title) },
  453. style = MaterialTheme.typography.titleLarge,
  454. modifier = Modifier.clickableNoIndication(
  455. onLongClick = {
  456. if (title.isNotBlank()) {
  457. context.copyToClipboard(
  458. title,
  459. title,
  460. )
  461. }
  462. },
  463. onClick = { if (title.isNotBlank()) doSearch(title, true) },
  464. ),
  465. )
  466. Spacer(modifier = Modifier.height(2.dp))
  467. Text(
  468. text = author?.takeIf { it.isNotBlank() }
  469. ?: stringResource(R.string.unknown_author),
  470. style = MaterialTheme.typography.titleSmall,
  471. modifier = Modifier
  472. .secondaryItemAlpha()
  473. .padding(top = 2.dp)
  474. .clickableNoIndication(
  475. onLongClick = {
  476. if (!author.isNullOrBlank()) {
  477. context.copyToClipboard(
  478. author,
  479. author,
  480. )
  481. }
  482. },
  483. onClick = { if (!author.isNullOrBlank()) doSearch(author, true) },
  484. ),
  485. )
  486. if (!artist.isNullOrBlank() && author != artist) {
  487. Text(
  488. text = artist,
  489. style = MaterialTheme.typography.titleSmall,
  490. modifier = Modifier
  491. .secondaryItemAlpha()
  492. .padding(top = 2.dp)
  493. .clickableNoIndication(
  494. onLongClick = { context.copyToClipboard(artist, artist) },
  495. onClick = { doSearch(artist, true) },
  496. ),
  497. )
  498. }
  499. Spacer(modifier = Modifier.height(4.dp))
  500. Row(
  501. modifier = Modifier.secondaryItemAlpha(),
  502. verticalAlignment = Alignment.CenterVertically,
  503. ) {
  504. Icon(
  505. imageVector = when (status) {
  506. SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
  507. SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
  508. SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
  509. SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
  510. SManga.CANCELLED.toLong() -> Icons.Outlined.Close
  511. SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
  512. else -> Icons.Outlined.Block
  513. },
  514. contentDescription = null,
  515. modifier = Modifier
  516. .padding(end = 4.dp)
  517. .size(16.dp),
  518. )
  519. ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
  520. Text(
  521. text = when (status) {
  522. SManga.ONGOING.toLong() -> stringResource(R.string.ongoing)
  523. SManga.COMPLETED.toLong() -> stringResource(R.string.completed)
  524. SManga.LICENSED.toLong() -> stringResource(R.string.licensed)
  525. SManga.PUBLISHING_FINISHED.toLong() -> stringResource(R.string.publishing_finished)
  526. SManga.CANCELLED.toLong() -> stringResource(R.string.cancelled)
  527. SManga.ON_HIATUS.toLong() -> stringResource(R.string.on_hiatus)
  528. else -> stringResource(R.string.unknown)
  529. },
  530. overflow = TextOverflow.Ellipsis,
  531. maxLines = 1,
  532. )
  533. DotSeparatorText()
  534. if (isStubSource) {
  535. Icon(
  536. imageVector = Icons.Filled.Warning,
  537. contentDescription = null,
  538. modifier = Modifier
  539. .padding(end = 4.dp)
  540. .size(16.dp),
  541. tint = MaterialTheme.colorScheme.error,
  542. )
  543. }
  544. Text(
  545. text = sourceName,
  546. modifier = Modifier.clickableNoIndication {
  547. doSearch(
  548. sourceName,
  549. false,
  550. )
  551. },
  552. overflow = TextOverflow.Ellipsis,
  553. maxLines = 1,
  554. )
  555. }
  556. }
  557. }
  558. }
  559. }
  560. @Composable
  561. private fun MangaSummary(
  562. expandedDescription: String,
  563. shrunkDescription: String,
  564. expanded: Boolean,
  565. modifier: Modifier = Modifier,
  566. ) {
  567. var expandedHeight by remember { mutableStateOf(0) }
  568. var shrunkHeight by remember { mutableStateOf(0) }
  569. val heightDelta = remember(expandedHeight, shrunkHeight) { expandedHeight - shrunkHeight }
  570. val animProgress by animateFloatAsState(if (expanded) 1f else 0f)
  571. val scrimHeight = with(LocalDensity.current) { remember { 24.sp.roundToPx() } }
  572. SubcomposeLayout(modifier = modifier.clipToBounds()) { constraints ->
  573. val shrunkPlaceable = subcompose("description-s") {
  574. Text(
  575. text = "\n\n", // Shows at least 3 lines
  576. style = MaterialTheme.typography.bodyMedium,
  577. )
  578. }.map { it.measure(constraints) }
  579. shrunkHeight = shrunkPlaceable.maxByOrNull { it.height }?.height ?: 0
  580. val expandedPlaceable = subcompose("description-l") {
  581. Text(
  582. text = expandedDescription,
  583. style = MaterialTheme.typography.bodyMedium,
  584. )
  585. }.map { it.measure(constraints) }
  586. expandedHeight = expandedPlaceable.maxByOrNull { it.height }?.height?.coerceAtLeast(shrunkHeight) ?: 0
  587. val actualPlaceable = subcompose("description") {
  588. SelectionContainer {
  589. Text(
  590. text = if (expanded) expandedDescription else shrunkDescription,
  591. maxLines = Int.MAX_VALUE,
  592. style = MaterialTheme.typography.bodyMedium,
  593. color = MaterialTheme.colorScheme.onBackground,
  594. modifier = Modifier.secondaryItemAlpha(),
  595. )
  596. }
  597. }.map { it.measure(constraints) }
  598. val scrimPlaceable = subcompose("scrim") {
  599. val colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background)
  600. Box(
  601. modifier = Modifier.background(Brush.verticalGradient(colors = colors)),
  602. contentAlignment = Alignment.Center,
  603. ) {
  604. val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_caret_down)
  605. Icon(
  606. painter = rememberAnimatedVectorPainter(image, !expanded),
  607. contentDescription = stringResource(if (expanded) R.string.manga_info_collapse else R.string.manga_info_expand),
  608. tint = MaterialTheme.colorScheme.onBackground,
  609. modifier = Modifier.background(Brush.radialGradient(colors = colors.asReversed())),
  610. )
  611. }
  612. }.map { it.measure(Constraints.fixed(width = constraints.maxWidth, height = scrimHeight)) }
  613. val currentHeight = shrunkHeight + ((heightDelta + scrimHeight) * animProgress).roundToInt()
  614. layout(constraints.maxWidth, currentHeight) {
  615. actualPlaceable.forEach {
  616. it.place(0, 0)
  617. }
  618. val scrimY = currentHeight - scrimHeight
  619. scrimPlaceable.forEach {
  620. it.place(0, scrimY)
  621. }
  622. }
  623. }
  624. }
  625. @Composable
  626. private fun TagsChip(
  627. text: String,
  628. onClick: () -> Unit,
  629. ) {
  630. CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
  631. SuggestionChip(
  632. onClick = onClick,
  633. label = { Text(text = text, style = MaterialTheme.typography.bodySmall) },
  634. border = null,
  635. colors = SuggestionChipDefaults.suggestionChipColors(
  636. containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
  637. labelColor = MaterialTheme.colorScheme.onSurface,
  638. ),
  639. )
  640. }
  641. }
  642. @Composable
  643. private fun RowScope.MangaActionButton(
  644. title: String,
  645. icon: ImageVector,
  646. color: Color,
  647. onClick: () -> Unit,
  648. onLongClick: (() -> Unit)? = null,
  649. ) {
  650. TextButton(
  651. onClick = onClick,
  652. modifier = Modifier.weight(1f),
  653. onLongClick = onLongClick,
  654. ) {
  655. Column(horizontalAlignment = Alignment.CenterHorizontally) {
  656. Icon(
  657. imageVector = icon,
  658. contentDescription = null,
  659. tint = color,
  660. modifier = Modifier.size(20.dp),
  661. )
  662. Spacer(Modifier.height(4.dp))
  663. Text(
  664. text = title,
  665. color = color,
  666. fontSize = 12.sp,
  667. textAlign = TextAlign.Center,
  668. )
  669. }
  670. }
  671. }