TrackInfoDialogHome.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. package eu.kanade.presentation.track
  2. import androidx.annotation.StringRes
  3. import androidx.compose.animation.animateContentSize
  4. import androidx.compose.foundation.background
  5. import androidx.compose.foundation.clickable
  6. import androidx.compose.foundation.combinedClickable
  7. import androidx.compose.foundation.layout.Arrangement
  8. import androidx.compose.foundation.layout.Box
  9. import androidx.compose.foundation.layout.Column
  10. import androidx.compose.foundation.layout.IntrinsicSize
  11. import androidx.compose.foundation.layout.Row
  12. import androidx.compose.foundation.layout.WindowInsets
  13. import androidx.compose.foundation.layout.fillMaxHeight
  14. import androidx.compose.foundation.layout.fillMaxWidth
  15. import androidx.compose.foundation.layout.height
  16. import androidx.compose.foundation.layout.padding
  17. import androidx.compose.foundation.layout.systemBars
  18. import androidx.compose.foundation.layout.windowInsetsPadding
  19. import androidx.compose.foundation.layout.wrapContentSize
  20. import androidx.compose.foundation.rememberScrollState
  21. import androidx.compose.foundation.shape.RoundedCornerShape
  22. import androidx.compose.foundation.verticalScroll
  23. import androidx.compose.material.icons.Icons
  24. import androidx.compose.material.icons.filled.MoreVert
  25. import androidx.compose.material3.DropdownMenuItem
  26. import androidx.compose.material3.HorizontalDivider
  27. import androidx.compose.material3.Icon
  28. import androidx.compose.material3.IconButton
  29. import androidx.compose.material3.MaterialTheme
  30. import androidx.compose.material3.Surface
  31. import androidx.compose.material3.Text
  32. import androidx.compose.material3.TextButton
  33. import androidx.compose.material3.VerticalDivider
  34. import androidx.compose.runtime.Composable
  35. import androidx.compose.runtime.getValue
  36. import androidx.compose.runtime.mutableStateOf
  37. import androidx.compose.runtime.remember
  38. import androidx.compose.runtime.setValue
  39. import androidx.compose.ui.Alignment
  40. import androidx.compose.ui.Modifier
  41. import androidx.compose.ui.draw.alpha
  42. import androidx.compose.ui.draw.clip
  43. import androidx.compose.ui.platform.LocalContext
  44. import androidx.compose.ui.res.stringResource
  45. import androidx.compose.ui.text.style.TextAlign
  46. import androidx.compose.ui.text.style.TextOverflow
  47. import androidx.compose.ui.tooling.preview.PreviewLightDark
  48. import androidx.compose.ui.tooling.preview.PreviewParameter
  49. import androidx.compose.ui.unit.dp
  50. import eu.kanade.domain.track.model.toDbTrack
  51. import eu.kanade.presentation.components.DropdownMenu
  52. import eu.kanade.presentation.theme.TachiyomiTheme
  53. import eu.kanade.presentation.track.components.TrackLogoIcon
  54. import eu.kanade.tachiyomi.R
  55. import eu.kanade.tachiyomi.data.track.Tracker
  56. import eu.kanade.tachiyomi.ui.manga.track.TrackItem
  57. import eu.kanade.tachiyomi.util.system.copyToClipboard
  58. import java.text.DateFormat
  59. private const val UnsetStatusTextAlpha = 0.5F
  60. @Composable
  61. fun TrackInfoDialogHome(
  62. trackItems: List<TrackItem>,
  63. dateFormat: DateFormat,
  64. onStatusClick: (TrackItem) -> Unit,
  65. onChapterClick: (TrackItem) -> Unit,
  66. onScoreClick: (TrackItem) -> Unit,
  67. onStartDateEdit: (TrackItem) -> Unit,
  68. onEndDateEdit: (TrackItem) -> Unit,
  69. onNewSearch: (TrackItem) -> Unit,
  70. onOpenInBrowser: (TrackItem) -> Unit,
  71. onRemoved: (TrackItem) -> Unit,
  72. ) {
  73. Column(
  74. modifier = Modifier
  75. .animateContentSize()
  76. .fillMaxWidth()
  77. .verticalScroll(rememberScrollState())
  78. .padding(16.dp)
  79. .windowInsetsPadding(WindowInsets.systemBars),
  80. verticalArrangement = Arrangement.spacedBy(24.dp),
  81. ) {
  82. trackItems.forEach { item ->
  83. if (item.track != null) {
  84. val supportsScoring = item.tracker.getScoreList().isNotEmpty()
  85. val supportsReadingDates = item.tracker.supportsReadingDates
  86. TrackInfoItem(
  87. title = item.track.title,
  88. tracker = item.tracker,
  89. status = item.tracker.getStatus(item.track.status.toInt()),
  90. onStatusClick = { onStatusClick(item) },
  91. chapters = "${item.track.lastChapterRead.toInt()}".let {
  92. val totalChapters = item.track.totalChapters
  93. if (totalChapters > 0) {
  94. // Add known total chapter count
  95. "$it / $totalChapters"
  96. } else {
  97. it
  98. }
  99. },
  100. onChaptersClick = { onChapterClick(item) },
  101. score = item.tracker.displayScore(item.track.toDbTrack())
  102. .takeIf { supportsScoring && item.track.score != 0.0 },
  103. onScoreClick = { onScoreClick(item) }
  104. .takeIf { supportsScoring },
  105. startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate) }
  106. .takeIf { supportsReadingDates && item.track.startDate != 0L },
  107. onStartDateClick = { onStartDateEdit(item) } // TODO
  108. .takeIf { supportsReadingDates },
  109. endDate = dateFormat.format(item.track.finishDate)
  110. .takeIf { supportsReadingDates && item.track.finishDate != 0L },
  111. onEndDateClick = { onEndDateEdit(item) }
  112. .takeIf { supportsReadingDates },
  113. onNewSearch = { onNewSearch(item) },
  114. onOpenInBrowser = { onOpenInBrowser(item) },
  115. onRemoved = { onRemoved(item) },
  116. )
  117. } else {
  118. TrackInfoItemEmpty(
  119. tracker = item.tracker,
  120. onNewSearch = { onNewSearch(item) },
  121. )
  122. }
  123. }
  124. }
  125. }
  126. @Composable
  127. private fun TrackInfoItem(
  128. title: String,
  129. tracker: Tracker,
  130. @StringRes status: Int?,
  131. onStatusClick: () -> Unit,
  132. chapters: String,
  133. onChaptersClick: () -> Unit,
  134. score: String?,
  135. onScoreClick: (() -> Unit)?,
  136. startDate: String?,
  137. onStartDateClick: (() -> Unit)?,
  138. endDate: String?,
  139. onEndDateClick: (() -> Unit)?,
  140. onNewSearch: () -> Unit,
  141. onOpenInBrowser: () -> Unit,
  142. onRemoved: () -> Unit,
  143. ) {
  144. val context = LocalContext.current
  145. Column {
  146. Row(
  147. verticalAlignment = Alignment.CenterVertically,
  148. ) {
  149. TrackLogoIcon(
  150. tracker = tracker,
  151. onClick = onOpenInBrowser,
  152. )
  153. Box(
  154. modifier = Modifier
  155. .height(48.dp)
  156. .weight(1f)
  157. .combinedClickable(
  158. onClick = onNewSearch,
  159. onLongClick = {
  160. context.copyToClipboard(title, title)
  161. },
  162. )
  163. .padding(start = 16.dp),
  164. contentAlignment = Alignment.CenterStart,
  165. ) {
  166. Text(
  167. text = title,
  168. maxLines = 1,
  169. overflow = TextOverflow.Ellipsis,
  170. style = MaterialTheme.typography.titleMedium,
  171. color = MaterialTheme.colorScheme.onSurface,
  172. )
  173. }
  174. VerticalDivider()
  175. TrackInfoItemMenu(
  176. onOpenInBrowser = onOpenInBrowser,
  177. onRemoved = onRemoved,
  178. )
  179. }
  180. Box(
  181. modifier = Modifier
  182. .padding(top = 12.dp)
  183. .clip(MaterialTheme.shapes.medium)
  184. .background(MaterialTheme.colorScheme.surface)
  185. .padding(8.dp)
  186. .clip(RoundedCornerShape(6.dp)),
  187. ) {
  188. Column {
  189. Row(modifier = Modifier.height(IntrinsicSize.Min)) {
  190. TrackDetailsItem(
  191. modifier = Modifier.weight(1f),
  192. text = status?.let { stringResource(it) } ?: "",
  193. onClick = onStatusClick,
  194. )
  195. VerticalDivider()
  196. TrackDetailsItem(
  197. modifier = Modifier.weight(1f),
  198. text = chapters,
  199. onClick = onChaptersClick,
  200. )
  201. if (onScoreClick != null) {
  202. VerticalDivider()
  203. TrackDetailsItem(
  204. modifier = Modifier
  205. .weight(1f)
  206. .alpha(if (score == null) UnsetStatusTextAlpha else 1f),
  207. text = score ?: stringResource(R.string.score),
  208. onClick = onScoreClick,
  209. )
  210. }
  211. }
  212. if (onStartDateClick != null && onEndDateClick != null) {
  213. HorizontalDivider()
  214. Row(modifier = Modifier.height(IntrinsicSize.Min)) {
  215. TrackDetailsItem(
  216. modifier = Modifier.weight(1F),
  217. text = startDate,
  218. placeholder = stringResource(R.string.track_started_reading_date),
  219. onClick = onStartDateClick,
  220. )
  221. VerticalDivider()
  222. TrackDetailsItem(
  223. modifier = Modifier.weight(1F),
  224. text = endDate,
  225. placeholder = stringResource(R.string.track_finished_reading_date),
  226. onClick = onEndDateClick,
  227. )
  228. }
  229. }
  230. }
  231. }
  232. }
  233. }
  234. @Composable
  235. private fun TrackDetailsItem(
  236. text: String?,
  237. onClick: () -> Unit,
  238. modifier: Modifier = Modifier,
  239. placeholder: String = "",
  240. ) {
  241. Box(
  242. modifier = modifier
  243. .clickable(onClick = onClick)
  244. .alpha(if (text == null) UnsetStatusTextAlpha else 1f)
  245. .fillMaxHeight()
  246. .padding(12.dp),
  247. contentAlignment = Alignment.Center,
  248. ) {
  249. Text(
  250. text = text ?: placeholder,
  251. maxLines = 2,
  252. overflow = TextOverflow.Ellipsis,
  253. style = MaterialTheme.typography.bodyMedium,
  254. textAlign = TextAlign.Center,
  255. color = MaterialTheme.colorScheme.onSurface,
  256. )
  257. }
  258. }
  259. @Composable
  260. private fun TrackInfoItemEmpty(
  261. tracker: Tracker,
  262. onNewSearch: () -> Unit,
  263. ) {
  264. Row(
  265. verticalAlignment = Alignment.CenterVertically,
  266. ) {
  267. TrackLogoIcon(tracker)
  268. TextButton(
  269. onClick = onNewSearch,
  270. modifier = Modifier
  271. .padding(start = 16.dp)
  272. .weight(1f),
  273. ) {
  274. Text(text = stringResource(R.string.add_tracking))
  275. }
  276. }
  277. }
  278. @Composable
  279. private fun TrackInfoItemMenu(
  280. onOpenInBrowser: () -> Unit,
  281. onRemoved: () -> Unit,
  282. ) {
  283. var expanded by remember { mutableStateOf(false) }
  284. Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
  285. IconButton(onClick = { expanded = true }) {
  286. Icon(
  287. imageVector = Icons.Default.MoreVert,
  288. contentDescription = stringResource(R.string.label_more),
  289. )
  290. }
  291. DropdownMenu(
  292. expanded = expanded,
  293. onDismissRequest = { expanded = false },
  294. ) {
  295. DropdownMenuItem(
  296. text = { Text(stringResource(R.string.action_open_in_browser)) },
  297. onClick = {
  298. onOpenInBrowser()
  299. expanded = false
  300. },
  301. )
  302. DropdownMenuItem(
  303. text = { Text(stringResource(R.string.action_remove)) },
  304. onClick = {
  305. onRemoved()
  306. expanded = false
  307. },
  308. )
  309. }
  310. }
  311. }
  312. @PreviewLightDark
  313. @Composable
  314. private fun TrackInfoDialogHomePreviews(
  315. @PreviewParameter(TrackInfoDialogHomePreviewProvider::class)
  316. content: @Composable () -> Unit,
  317. ) {
  318. TachiyomiTheme {
  319. Surface {
  320. content()
  321. }
  322. }
  323. }