SourcesScreen.kt 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package eu.kanade.presentation.browse
  2. import androidx.compose.foundation.clickable
  3. import androidx.compose.foundation.layout.Column
  4. import androidx.compose.foundation.layout.WindowInsets
  5. import androidx.compose.foundation.layout.asPaddingValues
  6. import androidx.compose.foundation.layout.fillMaxWidth
  7. import androidx.compose.foundation.layout.navigationBars
  8. import androidx.compose.foundation.layout.padding
  9. import androidx.compose.foundation.lazy.items
  10. import androidx.compose.material.icons.Icons
  11. import androidx.compose.material.icons.filled.PushPin
  12. import androidx.compose.material.icons.outlined.PushPin
  13. import androidx.compose.material3.AlertDialog
  14. import androidx.compose.material3.Icon
  15. import androidx.compose.material3.IconButton
  16. import androidx.compose.material3.LocalTextStyle
  17. import androidx.compose.material3.MaterialTheme
  18. import androidx.compose.material3.Text
  19. import androidx.compose.material3.TextButton
  20. import androidx.compose.runtime.Composable
  21. import androidx.compose.runtime.LaunchedEffect
  22. import androidx.compose.ui.Modifier
  23. import androidx.compose.ui.platform.LocalContext
  24. import androidx.compose.ui.res.stringResource
  25. import androidx.compose.ui.unit.dp
  26. import eu.kanade.domain.source.interactor.GetRemoteManga
  27. import eu.kanade.domain.source.model.Pin
  28. import eu.kanade.domain.source.model.Source
  29. import eu.kanade.presentation.browse.components.BaseSourceItem
  30. import eu.kanade.presentation.components.EmptyScreen
  31. import eu.kanade.presentation.components.LoadingScreen
  32. import eu.kanade.presentation.components.ScrollbarLazyColumn
  33. import eu.kanade.presentation.theme.header
  34. import eu.kanade.presentation.util.bottomNavPaddingValues
  35. import eu.kanade.presentation.util.horizontalPadding
  36. import eu.kanade.presentation.util.plus
  37. import eu.kanade.presentation.util.topPaddingValues
  38. import eu.kanade.tachiyomi.R
  39. import eu.kanade.tachiyomi.source.LocalSource
  40. import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
  41. import eu.kanade.tachiyomi.util.system.LocaleHelper
  42. import eu.kanade.tachiyomi.util.system.toast
  43. import kotlinx.coroutines.flow.collectLatest
  44. @Composable
  45. fun SourcesScreen(
  46. presenter: SourcesPresenter,
  47. onClickItem: (Source, String) -> Unit,
  48. onClickDisable: (Source) -> Unit,
  49. onClickPin: (Source) -> Unit,
  50. ) {
  51. val context = LocalContext.current
  52. when {
  53. presenter.isLoading -> LoadingScreen()
  54. presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen)
  55. else -> {
  56. SourceList(
  57. state = presenter,
  58. onClickItem = onClickItem,
  59. onClickDisable = onClickDisable,
  60. onClickPin = onClickPin,
  61. )
  62. }
  63. }
  64. LaunchedEffect(Unit) {
  65. presenter.events.collectLatest { event ->
  66. when (event) {
  67. SourcesPresenter.Event.FailedFetchingSources -> {
  68. context.toast(R.string.internal_error)
  69. }
  70. }
  71. }
  72. }
  73. }
  74. @Composable
  75. private fun SourceList(
  76. state: SourcesState,
  77. onClickItem: (Source, String) -> Unit,
  78. onClickDisable: (Source) -> Unit,
  79. onClickPin: (Source) -> Unit,
  80. ) {
  81. ScrollbarLazyColumn(
  82. contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
  83. ) {
  84. items(
  85. items = state.items,
  86. contentType = {
  87. when (it) {
  88. is SourceUiModel.Header -> "header"
  89. is SourceUiModel.Item -> "item"
  90. }
  91. },
  92. key = {
  93. when (it) {
  94. is SourceUiModel.Header -> it.hashCode()
  95. is SourceUiModel.Item -> "source-${it.source.key()}"
  96. }
  97. },
  98. ) { model ->
  99. when (model) {
  100. is SourceUiModel.Header -> {
  101. SourceHeader(
  102. modifier = Modifier.animateItemPlacement(),
  103. language = model.language,
  104. )
  105. }
  106. is SourceUiModel.Item -> SourceItem(
  107. modifier = Modifier.animateItemPlacement(),
  108. source = model.source,
  109. onClickItem = onClickItem,
  110. onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) },
  111. onClickPin = onClickPin,
  112. )
  113. }
  114. }
  115. }
  116. if (state.dialog != null) {
  117. val source = state.dialog!!.source
  118. SourceOptionsDialog(
  119. source = source,
  120. onClickPin = {
  121. onClickPin(source)
  122. state.dialog = null
  123. },
  124. onClickDisable = {
  125. onClickDisable(source)
  126. state.dialog = null
  127. },
  128. onDismiss = { state.dialog = null },
  129. )
  130. }
  131. }
  132. @Composable
  133. private fun SourceHeader(
  134. modifier: Modifier = Modifier,
  135. language: String,
  136. ) {
  137. val context = LocalContext.current
  138. Text(
  139. text = LocaleHelper.getSourceDisplayName(language, context),
  140. modifier = modifier
  141. .padding(horizontal = horizontalPadding, vertical = 8.dp),
  142. style = MaterialTheme.typography.header,
  143. )
  144. }
  145. @Composable
  146. private fun SourceItem(
  147. modifier: Modifier = Modifier,
  148. source: Source,
  149. onClickItem: (Source, String) -> Unit,
  150. onLongClickItem: (Source) -> Unit,
  151. onClickPin: (Source) -> Unit,
  152. ) {
  153. BaseSourceItem(
  154. modifier = modifier,
  155. source = source,
  156. onClickItem = { onClickItem(source, GetRemoteManga.QUERY_POPULAR) },
  157. onLongClickItem = { onLongClickItem(source) },
  158. action = {
  159. if (source.supportsLatest) {
  160. TextButton(onClick = { onClickItem(source, GetRemoteManga.QUERY_LATEST) }) {
  161. Text(
  162. text = stringResource(R.string.latest),
  163. style = LocalTextStyle.current.copy(
  164. color = MaterialTheme.colorScheme.primary,
  165. ),
  166. )
  167. }
  168. }
  169. SourcePinButton(
  170. isPinned = Pin.Pinned in source.pin,
  171. onClick = { onClickPin(source) },
  172. )
  173. },
  174. )
  175. }
  176. @Composable
  177. private fun SourcePinButton(
  178. isPinned: Boolean,
  179. onClick: () -> Unit,
  180. ) {
  181. val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
  182. val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground
  183. IconButton(onClick = onClick) {
  184. Icon(
  185. imageVector = icon,
  186. contentDescription = "",
  187. tint = tint,
  188. )
  189. }
  190. }
  191. @Composable
  192. private fun SourceOptionsDialog(
  193. source: Source,
  194. onClickPin: () -> Unit,
  195. onClickDisable: () -> Unit,
  196. onDismiss: () -> Unit,
  197. ) {
  198. AlertDialog(
  199. title = {
  200. Text(text = source.visualName)
  201. },
  202. text = {
  203. Column {
  204. val textId = if (Pin.Pinned in source.pin) R.string.action_unpin else R.string.action_pin
  205. Text(
  206. text = stringResource(textId),
  207. modifier = Modifier
  208. .clickable(onClick = onClickPin)
  209. .fillMaxWidth()
  210. .padding(vertical = 16.dp),
  211. )
  212. if (source.id != LocalSource.ID) {
  213. Text(
  214. text = stringResource(R.string.action_disable),
  215. modifier = Modifier
  216. .clickable(onClick = onClickDisable)
  217. .fillMaxWidth()
  218. .padding(vertical = 16.dp),
  219. )
  220. }
  221. }
  222. },
  223. onDismissRequest = onDismiss,
  224. confirmButton = {},
  225. )
  226. }
  227. sealed class SourceUiModel {
  228. data class Item(val source: Source) : SourceUiModel()
  229. data class Header(val language: String) : SourceUiModel()
  230. }