BrowseSourceScreen.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. package eu.kanade.presentation.browse
  2. import androidx.compose.animation.AnimatedVisibility
  3. import androidx.compose.foundation.layout.Arrangement
  4. import androidx.compose.foundation.layout.Column
  5. import androidx.compose.foundation.layout.PaddingValues
  6. import androidx.compose.foundation.layout.Row
  7. import androidx.compose.foundation.layout.navigationBarsPadding
  8. import androidx.compose.foundation.layout.size
  9. import androidx.compose.foundation.lazy.grid.GridCells
  10. import androidx.compose.material.icons.Icons
  11. import androidx.compose.material.icons.outlined.Favorite
  12. import androidx.compose.material.icons.outlined.FilterList
  13. import androidx.compose.material.icons.outlined.NewReleases
  14. import androidx.compose.material3.FilterChip
  15. import androidx.compose.material3.FilterChipDefaults
  16. import androidx.compose.material3.Icon
  17. import androidx.compose.material3.SnackbarDuration
  18. import androidx.compose.material3.SnackbarHost
  19. import androidx.compose.material3.SnackbarHostState
  20. import androidx.compose.material3.SnackbarResult
  21. import androidx.compose.material3.Text
  22. import androidx.compose.runtime.Composable
  23. import androidx.compose.runtime.LaunchedEffect
  24. import androidx.compose.runtime.State
  25. import androidx.compose.runtime.getValue
  26. import androidx.compose.runtime.remember
  27. import androidx.compose.ui.Modifier
  28. import androidx.compose.ui.platform.LocalContext
  29. import androidx.compose.ui.platform.LocalUriHandler
  30. import androidx.compose.ui.res.stringResource
  31. import androidx.compose.ui.unit.dp
  32. import androidx.paging.LoadState
  33. import androidx.paging.compose.LazyPagingItems
  34. import androidx.paging.compose.collectAsLazyPagingItems
  35. import eu.kanade.data.source.NoResultsException
  36. import eu.kanade.domain.manga.model.Manga
  37. import eu.kanade.domain.source.interactor.GetRemoteManga
  38. import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
  39. import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
  40. import eu.kanade.presentation.browse.components.BrowseSourceList
  41. import eu.kanade.presentation.browse.components.BrowseSourceToolbar
  42. import eu.kanade.presentation.components.AppStateBanners
  43. import eu.kanade.presentation.components.EmptyScreen
  44. import eu.kanade.presentation.components.ExtendedFloatingActionButton
  45. import eu.kanade.presentation.components.LoadingScreen
  46. import eu.kanade.presentation.components.Scaffold
  47. import eu.kanade.tachiyomi.R
  48. import eu.kanade.tachiyomi.source.LocalSource
  49. import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
  50. import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
  51. import eu.kanade.tachiyomi.ui.more.MoreController
  52. import eu.kanade.tachiyomi.widget.EmptyView
  53. @Composable
  54. fun BrowseSourceScreen(
  55. presenter: BrowseSourcePresenter,
  56. navigateUp: () -> Unit,
  57. onFabClick: () -> Unit,
  58. onMangaClick: (Manga) -> Unit,
  59. onMangaLongClick: (Manga) -> Unit,
  60. onWebViewClick: () -> Unit,
  61. incognitoMode: Boolean,
  62. downloadedOnlyMode: Boolean,
  63. ) {
  64. val columns by presenter.getColumnsPreferenceForCurrentOrientation()
  65. val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
  66. val snackbarHostState = remember { SnackbarHostState() }
  67. val uriHandler = LocalUriHandler.current
  68. val onHelpClick = {
  69. uriHandler.openUri(LocalSource.HELP_URL)
  70. }
  71. Scaffold(
  72. topBar = { scrollBehavior ->
  73. Column {
  74. BrowseSourceToolbar(
  75. state = presenter,
  76. source = presenter.source!!,
  77. displayMode = presenter.displayMode,
  78. onDisplayModeChange = { presenter.displayMode = it },
  79. navigateUp = navigateUp,
  80. onWebViewClick = onWebViewClick,
  81. onHelpClick = onHelpClick,
  82. onSearch = { presenter.search() },
  83. scrollBehavior = scrollBehavior,
  84. )
  85. AppStateBanners(downloadedOnlyMode, incognitoMode)
  86. }
  87. },
  88. floatingActionButton = {
  89. BrowseSourceFloatingActionButton(
  90. isVisible = presenter.filters.isNotEmpty() && presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
  91. onFabClick = onFabClick,
  92. )
  93. },
  94. snackbarHost = {
  95. SnackbarHost(hostState = snackbarHostState)
  96. },
  97. ) { paddingValues ->
  98. BrowseSourceContent(
  99. state = presenter,
  100. mangaList = mangaList,
  101. getMangaState = { presenter.getManga(it) },
  102. columns = columns,
  103. displayMode = presenter.displayMode,
  104. snackbarHostState = snackbarHostState,
  105. contentPadding = paddingValues,
  106. onWebViewClick = onWebViewClick,
  107. onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
  108. onLocalSourceHelpClick = onHelpClick,
  109. onMangaClick = onMangaClick,
  110. onMangaLongClick = onMangaLongClick,
  111. header = {
  112. Row(
  113. horizontalArrangement = Arrangement.spacedBy(8.dp),
  114. ) {
  115. FilterChip(
  116. selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Popular,
  117. onClick = {
  118. presenter.resetFilter()
  119. presenter.search(GetRemoteManga.QUERY_POPULAR)
  120. },
  121. leadingIcon = {
  122. Icon(
  123. imageVector = Icons.Outlined.Favorite,
  124. contentDescription = "",
  125. modifier = Modifier
  126. .size(FilterChipDefaults.IconSize),
  127. )
  128. },
  129. label = {
  130. Text(text = stringResource(R.string.popular))
  131. },
  132. )
  133. if (presenter.source?.supportsLatest == true) {
  134. FilterChip(
  135. selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Latest,
  136. onClick = {
  137. presenter.resetFilter()
  138. presenter.search(GetRemoteManga.QUERY_LATEST)
  139. },
  140. leadingIcon = {
  141. Icon(
  142. imageVector = Icons.Outlined.NewReleases,
  143. contentDescription = "",
  144. modifier = Modifier
  145. .size(FilterChipDefaults.IconSize),
  146. )
  147. },
  148. label = {
  149. Text(text = stringResource(R.string.latest))
  150. },
  151. )
  152. }
  153. if (presenter.filters.isNotEmpty()) {
  154. FilterChip(
  155. selected = presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
  156. onClick = onFabClick,
  157. leadingIcon = {
  158. Icon(
  159. imageVector = Icons.Outlined.FilterList,
  160. contentDescription = "",
  161. modifier = Modifier
  162. .size(FilterChipDefaults.IconSize),
  163. )
  164. },
  165. label = {
  166. Text(text = stringResource(R.string.action_filter))
  167. },
  168. )
  169. }
  170. }
  171. },
  172. )
  173. }
  174. }
  175. @Composable
  176. fun BrowseSourceFloatingActionButton(
  177. modifier: Modifier = Modifier.navigationBarsPadding(),
  178. isVisible: Boolean,
  179. onFabClick: () -> Unit,
  180. ) {
  181. AnimatedVisibility(visible = isVisible) {
  182. ExtendedFloatingActionButton(
  183. modifier = modifier,
  184. text = { Text(text = stringResource(R.string.action_filter)) },
  185. icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
  186. onClick = onFabClick,
  187. )
  188. }
  189. }
  190. @Composable
  191. fun BrowseSourceContent(
  192. state: BrowseSourceState,
  193. mangaList: LazyPagingItems<Manga>,
  194. getMangaState: @Composable ((Manga) -> State<Manga>),
  195. header: (@Composable () -> Unit)? = null,
  196. columns: GridCells,
  197. displayMode: LibraryDisplayMode,
  198. snackbarHostState: SnackbarHostState,
  199. contentPadding: PaddingValues,
  200. onWebViewClick: () -> Unit,
  201. onHelpClick: () -> Unit,
  202. onLocalSourceHelpClick: () -> Unit,
  203. onMangaClick: (Manga) -> Unit,
  204. onMangaLongClick: (Manga) -> Unit,
  205. ) {
  206. val context = LocalContext.current
  207. val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error }
  208. ?: mangaList.loadState.append.takeIf { it is LoadState.Error }
  209. val getErrorMessage: (LoadState.Error) -> String = { state ->
  210. when {
  211. state.error is NoResultsException -> context.getString(R.string.no_results_found)
  212. state.error.message == null -> ""
  213. state.error.message!!.startsWith("HTTP error") -> "${state.error.message}: ${context.getString(R.string.http_error_hint)}"
  214. else -> state.error.message!!
  215. }
  216. }
  217. LaunchedEffect(errorState) {
  218. if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
  219. val result = snackbarHostState.showSnackbar(
  220. message = getErrorMessage(errorState),
  221. actionLabel = context.getString(R.string.action_webview_refresh),
  222. duration = SnackbarDuration.Indefinite,
  223. )
  224. when (result) {
  225. SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
  226. SnackbarResult.ActionPerformed -> mangaList.refresh()
  227. }
  228. }
  229. }
  230. if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
  231. EmptyScreen(
  232. message = getErrorMessage(errorState),
  233. actions = if (state.source is LocalSource) {
  234. listOf(
  235. EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() },
  236. )
  237. } else {
  238. listOf(
  239. EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp) { mangaList.refresh() },
  240. EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { onWebViewClick() },
  241. EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { onHelpClick() },
  242. )
  243. },
  244. )
  245. return
  246. }
  247. if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
  248. LoadingScreen()
  249. return
  250. }
  251. when (displayMode) {
  252. LibraryDisplayMode.ComfortableGrid -> {
  253. BrowseSourceComfortableGrid(
  254. mangaList = mangaList,
  255. getMangaState = getMangaState,
  256. columns = columns,
  257. contentPadding = contentPadding,
  258. onMangaClick = onMangaClick,
  259. onMangaLongClick = onMangaLongClick,
  260. header = header,
  261. )
  262. }
  263. LibraryDisplayMode.List -> {
  264. BrowseSourceList(
  265. mangaList = mangaList,
  266. getMangaState = getMangaState,
  267. contentPadding = contentPadding,
  268. onMangaClick = onMangaClick,
  269. onMangaLongClick = onMangaLongClick,
  270. header = header,
  271. )
  272. }
  273. else -> {
  274. BrowseSourceCompactGrid(
  275. mangaList = mangaList,
  276. getMangaState = getMangaState,
  277. columns = columns,
  278. contentPadding = contentPadding,
  279. onMangaClick = onMangaClick,
  280. onMangaLongClick = onMangaLongClick,
  281. header = header,
  282. )
  283. }
  284. }
  285. }