HomeScreen.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. package eu.kanade.tachiyomi.ui.home
  2. import androidx.activity.compose.BackHandler
  3. import androidx.compose.animation.AnimatedContent
  4. import androidx.compose.animation.AnimatedVisibility
  5. import androidx.compose.animation.expandVertically
  6. import androidx.compose.animation.shrinkVertically
  7. import androidx.compose.foundation.layout.Box
  8. import androidx.compose.foundation.layout.Row
  9. import androidx.compose.foundation.layout.RowScope
  10. import androidx.compose.foundation.layout.WindowInsets
  11. import androidx.compose.foundation.layout.consumedWindowInsets
  12. import androidx.compose.foundation.layout.padding
  13. import androidx.compose.material3.Badge
  14. import androidx.compose.material3.BadgedBox
  15. import androidx.compose.material3.Icon
  16. import androidx.compose.material3.MaterialTheme
  17. import androidx.compose.material3.NavigationBarItem
  18. import androidx.compose.material3.NavigationRailItem
  19. import androidx.compose.material3.Text
  20. import androidx.compose.runtime.Composable
  21. import androidx.compose.runtime.CompositionLocalProvider
  22. import androidx.compose.runtime.LaunchedEffect
  23. import androidx.compose.runtime.getValue
  24. import androidx.compose.runtime.produceState
  25. import androidx.compose.runtime.rememberCoroutineScope
  26. import androidx.compose.ui.Alignment
  27. import androidx.compose.ui.Modifier
  28. import androidx.compose.ui.res.pluralStringResource
  29. import androidx.compose.ui.semantics.contentDescription
  30. import androidx.compose.ui.semantics.semantics
  31. import androidx.compose.ui.util.fastForEach
  32. import cafe.adriel.voyager.core.screen.Screen
  33. import cafe.adriel.voyager.navigator.LocalNavigator
  34. import cafe.adriel.voyager.navigator.currentOrThrow
  35. import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
  36. import cafe.adriel.voyager.navigator.tab.TabNavigator
  37. import eu.kanade.domain.library.service.LibraryPreferences
  38. import eu.kanade.domain.source.service.SourcePreferences
  39. import eu.kanade.presentation.components.NavigationBar
  40. import eu.kanade.presentation.components.NavigationRail
  41. import eu.kanade.presentation.components.Scaffold
  42. import eu.kanade.presentation.util.Tab
  43. import eu.kanade.presentation.util.Transition
  44. import eu.kanade.presentation.util.isTabletUi
  45. import eu.kanade.tachiyomi.R
  46. import eu.kanade.tachiyomi.ui.browse.BrowseTab
  47. import eu.kanade.tachiyomi.ui.history.HistoryTab
  48. import eu.kanade.tachiyomi.ui.library.LibraryTab
  49. import eu.kanade.tachiyomi.ui.manga.MangaScreen
  50. import eu.kanade.tachiyomi.ui.more.MoreTab
  51. import eu.kanade.tachiyomi.ui.updates.UpdatesTab
  52. import kotlinx.coroutines.channels.Channel
  53. import kotlinx.coroutines.flow.collectLatest
  54. import kotlinx.coroutines.flow.combine
  55. import kotlinx.coroutines.flow.receiveAsFlow
  56. import kotlinx.coroutines.launch
  57. import uy.kohesive.injekt.Injekt
  58. import uy.kohesive.injekt.api.get
  59. object HomeScreen : Screen {
  60. private val librarySearchEvent = Channel<String>()
  61. private val openTabEvent = Channel<Tab>()
  62. private val showBottomNavEvent = Channel<Boolean>()
  63. private val tabs = listOf(
  64. LibraryTab,
  65. UpdatesTab,
  66. HistoryTab,
  67. BrowseTab(),
  68. MoreTab(),
  69. )
  70. @Composable
  71. override fun Content() {
  72. val navigator = LocalNavigator.currentOrThrow
  73. TabNavigator(
  74. tab = LibraryTab,
  75. ) { tabNavigator ->
  76. // Provide usable navigator to content screen
  77. CompositionLocalProvider(LocalNavigator provides navigator) {
  78. Row(verticalAlignment = Alignment.CenterVertically) {
  79. if (isTabletUi()) {
  80. NavigationRail {
  81. tabs.fastForEach {
  82. NavigationRailItem(it)
  83. }
  84. }
  85. }
  86. Scaffold(
  87. bottomBar = {
  88. if (!isTabletUi()) {
  89. val bottomNavVisible by produceState(initialValue = true) {
  90. showBottomNavEvent.receiveAsFlow().collectLatest { value = it }
  91. }
  92. AnimatedVisibility(
  93. visible = bottomNavVisible,
  94. enter = expandVertically(),
  95. exit = shrinkVertically(),
  96. ) {
  97. NavigationBar {
  98. tabs.fastForEach {
  99. NavigationBarItem(it)
  100. }
  101. }
  102. }
  103. }
  104. },
  105. contentWindowInsets = WindowInsets(0),
  106. ) { contentPadding ->
  107. Box(
  108. modifier = Modifier
  109. .padding(contentPadding)
  110. .consumedWindowInsets(contentPadding),
  111. ) {
  112. AnimatedContent(
  113. targetState = tabNavigator.current,
  114. transitionSpec = { Transition.OneWayFade },
  115. content = {
  116. tabNavigator.saveableState(key = "currentTab", it) {
  117. it.Content()
  118. }
  119. },
  120. )
  121. }
  122. }
  123. }
  124. }
  125. val goToLibraryTab = { tabNavigator.current = LibraryTab }
  126. BackHandler(
  127. enabled = tabNavigator.current != LibraryTab,
  128. onBack = goToLibraryTab,
  129. )
  130. LaunchedEffect(Unit) {
  131. launch {
  132. librarySearchEvent.receiveAsFlow().collectLatest {
  133. goToLibraryTab()
  134. LibraryTab.search(it)
  135. }
  136. }
  137. launch {
  138. openTabEvent.receiveAsFlow().collectLatest {
  139. tabNavigator.current = when (it) {
  140. is Tab.Library -> LibraryTab
  141. Tab.Updates -> UpdatesTab
  142. Tab.History -> HistoryTab
  143. is Tab.Browse -> BrowseTab(it.toExtensions)
  144. is Tab.More -> MoreTab(it.toDownloads)
  145. }
  146. if (it is Tab.Library && it.mangaIdToOpen != null) {
  147. navigator.push(MangaScreen(it.mangaIdToOpen))
  148. }
  149. }
  150. }
  151. }
  152. }
  153. }
  154. @Composable
  155. private fun RowScope.NavigationBarItem(tab: eu.kanade.presentation.util.Tab) {
  156. val tabNavigator = LocalTabNavigator.current
  157. val navigator = LocalNavigator.currentOrThrow
  158. val scope = rememberCoroutineScope()
  159. val selected = tabNavigator.current::class == tab::class
  160. NavigationBarItem(
  161. selected = selected,
  162. onClick = {
  163. if (!selected) {
  164. tabNavigator.current = tab
  165. } else {
  166. scope.launch { tab.onReselect(navigator) }
  167. }
  168. },
  169. icon = { NavigationIconItem(tab) },
  170. label = {
  171. Text(
  172. text = tab.options.title,
  173. style = MaterialTheme.typography.labelLarge,
  174. )
  175. },
  176. alwaysShowLabel = true,
  177. )
  178. }
  179. @Composable
  180. fun NavigationRailItem(tab: eu.kanade.presentation.util.Tab) {
  181. val tabNavigator = LocalTabNavigator.current
  182. val navigator = LocalNavigator.currentOrThrow
  183. val scope = rememberCoroutineScope()
  184. val selected = tabNavigator.current::class == tab::class
  185. NavigationRailItem(
  186. selected = selected,
  187. onClick = {
  188. if (!selected) {
  189. tabNavigator.current = tab
  190. } else {
  191. scope.launch { tab.onReselect(navigator) }
  192. }
  193. },
  194. icon = { NavigationIconItem(tab) },
  195. label = {
  196. Text(
  197. text = tab.options.title,
  198. style = MaterialTheme.typography.labelLarge,
  199. )
  200. },
  201. alwaysShowLabel = true,
  202. )
  203. }
  204. @Composable
  205. private fun NavigationIconItem(tab: eu.kanade.presentation.util.Tab) {
  206. BadgedBox(
  207. badge = {
  208. when {
  209. tab is UpdatesTab -> {
  210. val count by produceState(initialValue = 0) {
  211. val pref = Injekt.get<LibraryPreferences>()
  212. combine(
  213. pref.showUpdatesNavBadge().changes(),
  214. pref.unreadUpdatesCount().changes(),
  215. ) { show, count -> if (show) count else 0 }
  216. .collectLatest { value = it }
  217. }
  218. if (count > 0) {
  219. Badge {
  220. val desc = pluralStringResource(
  221. id = R.plurals.notification_chapters_generic,
  222. count = count,
  223. count,
  224. )
  225. Text(
  226. text = count.toString(),
  227. modifier = Modifier.semantics { contentDescription = desc },
  228. )
  229. }
  230. }
  231. }
  232. BrowseTab::class.isInstance(tab) -> {
  233. val count by produceState(initialValue = 0) {
  234. Injekt.get<SourcePreferences>().extensionUpdatesCount().changes()
  235. .collectLatest { value = it }
  236. }
  237. if (count > 0) {
  238. Badge {
  239. val desc = pluralStringResource(
  240. id = R.plurals.update_check_notification_ext_updates,
  241. count = count,
  242. count,
  243. )
  244. Text(
  245. text = count.toString(),
  246. modifier = Modifier.semantics { contentDescription = desc },
  247. )
  248. }
  249. }
  250. }
  251. }
  252. },
  253. ) {
  254. Icon(painter = tab.options.icon!!, contentDescription = tab.options.title)
  255. }
  256. }
  257. suspend fun search(query: String) {
  258. librarySearchEvent.send(query)
  259. }
  260. suspend fun openTab(tab: Tab) {
  261. openTabEvent.send(tab)
  262. }
  263. suspend fun showBottomNav(show: Boolean) {
  264. showBottomNavEvent.send(show)
  265. }
  266. sealed class Tab {
  267. data class Library(val mangaIdToOpen: Long? = null) : Tab()
  268. object Updates : Tab()
  269. object History : Tab()
  270. data class Browse(val toExtensions: Boolean = false) : Tab()
  271. data class More(val toDownloads: Boolean) : Tab()
  272. }
  273. }