123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- package eu.kanade.tachiyomi.ui.home
- import androidx.activity.compose.BackHandler
- import androidx.compose.animation.AnimatedContent
- import androidx.compose.animation.AnimatedVisibility
- import androidx.compose.animation.expandVertically
- import androidx.compose.animation.shrinkVertically
- import androidx.compose.foundation.layout.Box
- import androidx.compose.foundation.layout.Row
- import androidx.compose.foundation.layout.RowScope
- import androidx.compose.foundation.layout.WindowInsets
- import androidx.compose.foundation.layout.consumedWindowInsets
- import androidx.compose.foundation.layout.padding
- import androidx.compose.material3.Badge
- import androidx.compose.material3.BadgedBox
- import androidx.compose.material3.Icon
- import androidx.compose.material3.MaterialTheme
- import androidx.compose.material3.NavigationBarItem
- import androidx.compose.material3.NavigationRailItem
- import androidx.compose.material3.Text
- import androidx.compose.runtime.Composable
- import androidx.compose.runtime.CompositionLocalProvider
- import androidx.compose.runtime.LaunchedEffect
- import androidx.compose.runtime.getValue
- import androidx.compose.runtime.produceState
- import androidx.compose.runtime.rememberCoroutineScope
- import androidx.compose.ui.Alignment
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.res.pluralStringResource
- import androidx.compose.ui.semantics.contentDescription
- import androidx.compose.ui.semantics.semantics
- import androidx.compose.ui.util.fastForEach
- import cafe.adriel.voyager.core.screen.Screen
- import cafe.adriel.voyager.navigator.LocalNavigator
- import cafe.adriel.voyager.navigator.currentOrThrow
- import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
- import cafe.adriel.voyager.navigator.tab.TabNavigator
- import eu.kanade.domain.library.service.LibraryPreferences
- import eu.kanade.domain.source.service.SourcePreferences
- import eu.kanade.presentation.components.NavigationBar
- import eu.kanade.presentation.components.NavigationRail
- import eu.kanade.presentation.components.Scaffold
- import eu.kanade.presentation.util.Tab
- import eu.kanade.presentation.util.Transition
- import eu.kanade.presentation.util.isTabletUi
- import eu.kanade.tachiyomi.R
- import eu.kanade.tachiyomi.ui.browse.BrowseTab
- import eu.kanade.tachiyomi.ui.history.HistoryTab
- import eu.kanade.tachiyomi.ui.library.LibraryTab
- import eu.kanade.tachiyomi.ui.manga.MangaScreen
- import eu.kanade.tachiyomi.ui.more.MoreTab
- import eu.kanade.tachiyomi.ui.updates.UpdatesTab
- import kotlinx.coroutines.channels.Channel
- import kotlinx.coroutines.flow.collectLatest
- import kotlinx.coroutines.flow.combine
- import kotlinx.coroutines.flow.receiveAsFlow
- import kotlinx.coroutines.launch
- import uy.kohesive.injekt.Injekt
- import uy.kohesive.injekt.api.get
- object HomeScreen : Screen {
- private val librarySearchEvent = Channel<String>()
- private val openTabEvent = Channel<Tab>()
- private val showBottomNavEvent = Channel<Boolean>()
- private val tabs = listOf(
- LibraryTab,
- UpdatesTab,
- HistoryTab,
- BrowseTab(),
- MoreTab(),
- )
- @Composable
- override fun Content() {
- val navigator = LocalNavigator.currentOrThrow
- TabNavigator(
- tab = LibraryTab,
- ) { tabNavigator ->
- // Provide usable navigator to content screen
- CompositionLocalProvider(LocalNavigator provides navigator) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- if (isTabletUi()) {
- NavigationRail {
- tabs.fastForEach {
- NavigationRailItem(it)
- }
- }
- }
- Scaffold(
- bottomBar = {
- if (!isTabletUi()) {
- val bottomNavVisible by produceState(initialValue = true) {
- showBottomNavEvent.receiveAsFlow().collectLatest { value = it }
- }
- AnimatedVisibility(
- visible = bottomNavVisible,
- enter = expandVertically(),
- exit = shrinkVertically(),
- ) {
- NavigationBar {
- tabs.fastForEach {
- NavigationBarItem(it)
- }
- }
- }
- }
- },
- contentWindowInsets = WindowInsets(0),
- ) { contentPadding ->
- Box(
- modifier = Modifier
- .padding(contentPadding)
- .consumedWindowInsets(contentPadding),
- ) {
- AnimatedContent(
- targetState = tabNavigator.current,
- transitionSpec = { Transition.OneWayFade },
- content = {
- tabNavigator.saveableState(key = "currentTab", it) {
- it.Content()
- }
- },
- )
- }
- }
- }
- }
- val goToLibraryTab = { tabNavigator.current = LibraryTab }
- BackHandler(
- enabled = tabNavigator.current != LibraryTab,
- onBack = goToLibraryTab,
- )
- LaunchedEffect(Unit) {
- launch {
- librarySearchEvent.receiveAsFlow().collectLatest {
- goToLibraryTab()
- LibraryTab.search(it)
- }
- }
- launch {
- openTabEvent.receiveAsFlow().collectLatest {
- tabNavigator.current = when (it) {
- is Tab.Library -> LibraryTab
- Tab.Updates -> UpdatesTab
- Tab.History -> HistoryTab
- is Tab.Browse -> BrowseTab(it.toExtensions)
- is Tab.More -> MoreTab(it.toDownloads)
- }
- if (it is Tab.Library && it.mangaIdToOpen != null) {
- navigator.push(MangaScreen(it.mangaIdToOpen))
- }
- }
- }
- }
- }
- }
- @Composable
- private fun RowScope.NavigationBarItem(tab: eu.kanade.presentation.util.Tab) {
- val tabNavigator = LocalTabNavigator.current
- val navigator = LocalNavigator.currentOrThrow
- val scope = rememberCoroutineScope()
- val selected = tabNavigator.current::class == tab::class
- NavigationBarItem(
- selected = selected,
- onClick = {
- if (!selected) {
- tabNavigator.current = tab
- } else {
- scope.launch { tab.onReselect(navigator) }
- }
- },
- icon = { NavigationIconItem(tab) },
- label = {
- Text(
- text = tab.options.title,
- style = MaterialTheme.typography.labelLarge,
- )
- },
- alwaysShowLabel = true,
- )
- }
- @Composable
- fun NavigationRailItem(tab: eu.kanade.presentation.util.Tab) {
- val tabNavigator = LocalTabNavigator.current
- val navigator = LocalNavigator.currentOrThrow
- val scope = rememberCoroutineScope()
- val selected = tabNavigator.current::class == tab::class
- NavigationRailItem(
- selected = selected,
- onClick = {
- if (!selected) {
- tabNavigator.current = tab
- } else {
- scope.launch { tab.onReselect(navigator) }
- }
- },
- icon = { NavigationIconItem(tab) },
- label = {
- Text(
- text = tab.options.title,
- style = MaterialTheme.typography.labelLarge,
- )
- },
- alwaysShowLabel = true,
- )
- }
- @Composable
- private fun NavigationIconItem(tab: eu.kanade.presentation.util.Tab) {
- BadgedBox(
- badge = {
- when {
- tab is UpdatesTab -> {
- val count by produceState(initialValue = 0) {
- val pref = Injekt.get<LibraryPreferences>()
- combine(
- pref.showUpdatesNavBadge().changes(),
- pref.unreadUpdatesCount().changes(),
- ) { show, count -> if (show) count else 0 }
- .collectLatest { value = it }
- }
- if (count > 0) {
- Badge {
- val desc = pluralStringResource(
- id = R.plurals.notification_chapters_generic,
- count = count,
- count,
- )
- Text(
- text = count.toString(),
- modifier = Modifier.semantics { contentDescription = desc },
- )
- }
- }
- }
- BrowseTab::class.isInstance(tab) -> {
- val count by produceState(initialValue = 0) {
- Injekt.get<SourcePreferences>().extensionUpdatesCount().changes()
- .collectLatest { value = it }
- }
- if (count > 0) {
- Badge {
- val desc = pluralStringResource(
- id = R.plurals.update_check_notification_ext_updates,
- count = count,
- count,
- )
- Text(
- text = count.toString(),
- modifier = Modifier.semantics { contentDescription = desc },
- )
- }
- }
- }
- }
- },
- ) {
- Icon(painter = tab.options.icon!!, contentDescription = tab.options.title)
- }
- }
- suspend fun search(query: String) {
- librarySearchEvent.send(query)
- }
- suspend fun openTab(tab: Tab) {
- openTabEvent.send(tab)
- }
- suspend fun showBottomNav(show: Boolean) {
- showBottomNavEvent.send(show)
- }
- sealed class Tab {
- data class Library(val mangaIdToOpen: Long? = null) : Tab()
- object Updates : Tab()
- object History : Tab()
- data class Browse(val toExtensions: Boolean = false) : Tab()
- data class More(val toDownloads: Boolean) : Tab()
- }
- }
|