SourcesScreen.kt 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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.collectAsState
  22. import androidx.compose.runtime.getValue
  23. import androidx.compose.runtime.mutableStateOf
  24. import androidx.compose.runtime.remember
  25. import androidx.compose.runtime.setValue
  26. import androidx.compose.ui.Modifier
  27. import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
  28. import androidx.compose.ui.input.nestedscroll.nestedScroll
  29. import androidx.compose.ui.platform.LocalContext
  30. import androidx.compose.ui.res.stringResource
  31. import androidx.compose.ui.unit.dp
  32. import eu.kanade.domain.source.model.Pin
  33. import eu.kanade.domain.source.model.Source
  34. import eu.kanade.presentation.browse.components.BaseSourceItem
  35. import eu.kanade.presentation.components.EmptyScreen
  36. import eu.kanade.presentation.components.LoadingScreen
  37. import eu.kanade.presentation.components.ScrollbarLazyColumn
  38. import eu.kanade.presentation.theme.header
  39. import eu.kanade.presentation.util.horizontalPadding
  40. import eu.kanade.presentation.util.plus
  41. import eu.kanade.presentation.util.topPaddingValues
  42. import eu.kanade.tachiyomi.R
  43. import eu.kanade.tachiyomi.source.LocalSource
  44. import eu.kanade.tachiyomi.ui.browse.source.SourceState
  45. import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
  46. import eu.kanade.tachiyomi.util.system.LocaleHelper
  47. @Composable
  48. fun SourcesScreen(
  49. nestedScrollInterop: NestedScrollConnection,
  50. presenter: SourcesPresenter,
  51. onClickItem: (Source) -> Unit,
  52. onClickDisable: (Source) -> Unit,
  53. onClickLatest: (Source) -> Unit,
  54. onClickPin: (Source) -> Unit,
  55. ) {
  56. val state by presenter.state.collectAsState()
  57. when (state) {
  58. is SourceState.Loading -> LoadingScreen()
  59. is SourceState.Error -> Text(text = (state as SourceState.Error).error.message!!)
  60. is SourceState.Success -> SourceList(
  61. nestedScrollConnection = nestedScrollInterop,
  62. list = (state as SourceState.Success).uiModels,
  63. onClickItem = onClickItem,
  64. onClickDisable = onClickDisable,
  65. onClickLatest = onClickLatest,
  66. onClickPin = onClickPin,
  67. )
  68. }
  69. }
  70. @Composable
  71. fun SourceList(
  72. nestedScrollConnection: NestedScrollConnection,
  73. list: List<SourceUiModel>,
  74. onClickItem: (Source) -> Unit,
  75. onClickDisable: (Source) -> Unit,
  76. onClickLatest: (Source) -> Unit,
  77. onClickPin: (Source) -> Unit,
  78. ) {
  79. if (list.isEmpty()) {
  80. EmptyScreen(textResource = R.string.source_empty_screen)
  81. return
  82. }
  83. var sourceState by remember { mutableStateOf<Source?>(null) }
  84. ScrollbarLazyColumn(
  85. modifier = Modifier.nestedScroll(nestedScrollConnection),
  86. contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
  87. ) {
  88. items(
  89. items = list,
  90. contentType = {
  91. when (it) {
  92. is SourceUiModel.Header -> "header"
  93. is SourceUiModel.Item -> "item"
  94. }
  95. },
  96. key = {
  97. when (it) {
  98. is SourceUiModel.Header -> it.hashCode()
  99. is SourceUiModel.Item -> it.source.key()
  100. }
  101. },
  102. ) { model ->
  103. when (model) {
  104. is SourceUiModel.Header -> {
  105. SourceHeader(
  106. modifier = Modifier.animateItemPlacement(),
  107. language = model.language,
  108. )
  109. }
  110. is SourceUiModel.Item -> SourceItem(
  111. modifier = Modifier.animateItemPlacement(),
  112. source = model.source,
  113. onClickItem = onClickItem,
  114. onLongClickItem = { sourceState = it },
  115. onClickLatest = onClickLatest,
  116. onClickPin = onClickPin,
  117. )
  118. }
  119. }
  120. }
  121. if (sourceState != null) {
  122. SourceOptionsDialog(
  123. source = sourceState!!,
  124. onClickPin = {
  125. onClickPin(sourceState!!)
  126. sourceState = null
  127. },
  128. onClickDisable = {
  129. onClickDisable(sourceState!!)
  130. sourceState = null
  131. },
  132. onDismiss = { sourceState = null },
  133. )
  134. }
  135. }
  136. @Composable
  137. fun SourceHeader(
  138. modifier: Modifier = Modifier,
  139. language: String,
  140. ) {
  141. val context = LocalContext.current
  142. Text(
  143. text = LocaleHelper.getSourceDisplayName(language, context),
  144. modifier = modifier
  145. .padding(horizontal = horizontalPadding, vertical = 8.dp),
  146. style = MaterialTheme.typography.header,
  147. )
  148. }
  149. @Composable
  150. fun SourceItem(
  151. modifier: Modifier = Modifier,
  152. source: Source,
  153. onClickItem: (Source) -> Unit,
  154. onLongClickItem: (Source) -> Unit,
  155. onClickLatest: (Source) -> Unit,
  156. onClickPin: (Source) -> Unit,
  157. ) {
  158. BaseSourceItem(
  159. modifier = modifier,
  160. source = source,
  161. onClickItem = { onClickItem(source) },
  162. onLongClickItem = { onLongClickItem(source) },
  163. action = { source ->
  164. if (source.supportsLatest) {
  165. TextButton(onClick = { onClickLatest(source) }) {
  166. Text(
  167. text = stringResource(R.string.latest),
  168. style = LocalTextStyle.current.copy(
  169. color = MaterialTheme.colorScheme.primary,
  170. ),
  171. )
  172. }
  173. }
  174. SourcePinButton(
  175. isPinned = Pin.Pinned in source.pin,
  176. onClick = { onClickPin(source) },
  177. )
  178. },
  179. )
  180. }
  181. @Composable
  182. fun SourcePinButton(
  183. isPinned: Boolean,
  184. onClick: () -> Unit,
  185. ) {
  186. val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
  187. val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground
  188. IconButton(onClick = onClick) {
  189. Icon(
  190. imageVector = icon,
  191. contentDescription = "",
  192. tint = tint,
  193. )
  194. }
  195. }
  196. @Composable
  197. fun SourceOptionsDialog(
  198. source: Source,
  199. onClickPin: () -> Unit,
  200. onClickDisable: () -> Unit,
  201. onDismiss: () -> Unit,
  202. ) {
  203. AlertDialog(
  204. title = {
  205. Text(text = source.nameWithLanguage)
  206. },
  207. text = {
  208. Column {
  209. val textId = if (Pin.Pinned in source.pin) R.string.action_unpin else R.string.action_pin
  210. Text(
  211. text = stringResource(id = textId),
  212. modifier = Modifier
  213. .clickable(onClick = onClickPin)
  214. .fillMaxWidth()
  215. .padding(vertical = 16.dp),
  216. )
  217. if (source.id != LocalSource.ID) {
  218. Text(
  219. text = stringResource(R.string.action_disable),
  220. modifier = Modifier
  221. .clickable(onClick = onClickDisable)
  222. .fillMaxWidth()
  223. .padding(vertical = 16.dp),
  224. )
  225. }
  226. }
  227. },
  228. onDismissRequest = onDismiss,
  229. confirmButton = {},
  230. )
  231. }
  232. sealed class SourceUiModel {
  233. data class Item(val source: Source) : SourceUiModel()
  234. data class Header(val language: String) : SourceUiModel()
  235. }