Merge Voyager screens (#8656)

* Merge Voyager screens

* cleanups
This commit is contained in:
Ivan Iskandar
2022-12-03 10:35:30 +07:00
committed by GitHub
parent 5313a5d5d2
commit 3d66eaea83
67 changed files with 1180 additions and 1991 deletions
@@ -0,0 +1,288 @@
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()
}
}