Initial conversion of browse tabs to full Compose

TODO:
- Global search should launch a controller with the search textfield focused. This is pending a Compose rewrite of that screen.
- Better migrate sort UI
- Extensions search
This commit is contained in:
arkon
2022-08-29 17:18:06 -04:00
parent 084e6a964e
commit 92e83f702c
32 changed files with 458 additions and 701 deletions

View File

@@ -0,0 +1,84 @@
package eu.kanade.presentation.browse
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.launch
@Composable
fun BrowseScreen(
startIndex: Int? = null,
tabs: List<BrowseTab>,
) {
val scope = rememberCoroutineScope()
val state = rememberPagerState()
LaunchedEffect(startIndex) {
if (startIndex != null) {
state.scrollToPage(startIndex)
}
}
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(R.string.browse),
actions = {
AppBarActions(tabs[state.currentPage].actions)
},
)
},
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
TabRow(
selectedTabIndex = state.currentPage,
indicator = { TabIndicator(it[state.currentPage]) },
) {
tabs.forEachIndexed { index, tab ->
Tab(
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
text = {
TabText(stringResource(tab.titleRes), tab.badgeNumber, state.currentPage == index)
},
)
}
}
HorizontalPager(
count = tabs.size,
modifier = Modifier.fillMaxSize(),
state = state,
verticalAlignment = Alignment.Top,
) { page ->
tabs[page].content()
}
}
}
}
data class BrowseTab(
@StringRes val titleRes: Int,
val badgeNumber: Int? = null,
val actions: List<AppBar.Action> = emptyList(),
val content: @Composable () -> Unit,
)

View File

@@ -22,15 +22,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -57,7 +54,6 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
fun ExtensionScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionsPresenter,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
@@ -68,10 +64,8 @@ fun ExtensionScreen(
onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit,
onLaunched: () -> Unit,
) {
SwipeRefresh(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = rememberSwipeRefreshState(presenter.isRefreshing),
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
onRefresh = onRefresh,
@@ -90,7 +84,6 @@ fun ExtensionScreen(
onTrustExtension = onTrustExtension,
onOpenExtension = onOpenExtension,
onClickUpdateAll = onClickUpdateAll,
onLaunched = onLaunched,
)
}
}
@@ -108,7 +101,6 @@ fun ExtensionContent(
onTrustExtension: (Extension.Untrusted) -> Unit,
onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit,
onLaunched: () -> Unit,
) {
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
@@ -187,9 +179,6 @@ fun ExtensionContent(
}
},
)
LaunchedEffect(Unit) {
onLaunched()
}
}
}
}

View File

@@ -10,6 +10,7 @@ interface ExtensionsState {
val isLoading: Boolean
val isRefreshing: Boolean
val items: List<ExtensionUiModel>
val updates: Int
val isEmpty: Boolean
}
@@ -21,5 +22,6 @@ class ExtensionsStateImpl : ExtensionsState {
override var isLoading: Boolean by mutableStateOf(true)
override var isRefreshing: Boolean by mutableStateOf(false)
override var items: List<ExtensionUiModel> by mutableStateOf(emptyList())
override var updates: Int by mutableStateOf(0)
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@@ -8,17 +8,17 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem
import eu.kanade.presentation.browse.components.SourceIcon
@@ -39,7 +39,6 @@ import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable
fun MigrateSourceScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: MigrationSourcesPresenter,
onClickItem: (Source) -> Unit,
) {
@@ -49,28 +48,44 @@ fun MigrateSourceScreen(
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library)
else ->
MigrateSourceList(
nestedScrollInterop = nestedScrollInterop,
list = presenter.items,
onClickItem = onClickItem,
onLongClickItem = { source ->
val sourceId = source.id.toString()
context.copyToClipboard(sourceId, sourceId)
},
sortingMode = presenter.sortingMode,
onToggleSortingMode = { presenter.toggleSortingMode() },
sortingDirection = presenter.sortingDirection,
onToggleSortingDirection = { presenter.toggleSortingDirection() },
)
}
}
@Composable
fun MigrateSourceList(
nestedScrollInterop: NestedScrollConnection,
list: List<Pair<Source, Long>>,
onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,
sortingMode: SetMigrateSorting.Mode,
onToggleSortingMode: () -> Unit,
sortingDirection: SetMigrateSorting.Direction,
onToggleSortingDirection: () -> Unit,
) {
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) {
stickyHeader {
Row {
Button(onClick = onToggleSortingMode) {
Text(sortingMode.toString())
}
Button(onClick = onToggleSortingDirection) {
Text(sortingDirection.toString())
}
}
}
item(key = "title") {
Text(
text = stringResource(R.string.migration_selection_prompt),

View File

@@ -4,12 +4,15 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source
interface MigrateSourceState {
val isLoading: Boolean
val items: List<Pair<Source, Long>>
val isEmpty: Boolean
val sortingMode: SetMigrateSorting.Mode
val sortingDirection: SetMigrateSorting.Direction
}
fun MigrateSourceState(): MigrateSourceState {
@@ -20,4 +23,6 @@ class MigrateSourceStateImpl : MigrateSourceState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
override var sortingMode: SetMigrateSorting.Mode by mutableStateOf(SetMigrateSorting.Mode.ALPHABETICAL)
override var sortingDirection: SetMigrateSorting.Direction by mutableStateOf(SetMigrateSorting.Direction.ASCENDING)
}

View File

@@ -21,8 +21,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -40,14 +38,12 @@ import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter.Dialog
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable
fun SourcesScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: SourcesPresenter,
onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit,
@@ -60,7 +56,6 @@ fun SourcesScreen(
presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen)
else -> {
SourceList(
nestedScrollConnection = nestedScrollInterop,
state = presenter,
onClickItem = onClickItem,
onClickDisable = onClickDisable,
@@ -82,7 +77,6 @@ fun SourcesScreen(
@Composable
fun SourceList(
nestedScrollConnection: NestedScrollConnection,
state: SourcesState,
onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit,
@@ -90,7 +84,6 @@ fun SourceList(
onClickPin: (Source) -> Unit,
) {
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection),
contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) {
items(
@@ -119,7 +112,7 @@ fun SourceList(
modifier = Modifier.animateItemPlacement(),
source = model.source,
onClickItem = onClickItem,
onLongClickItem = { state.dialog = Dialog(it) },
onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) },
onClickLatest = onClickLatest,
onClickPin = onClickPin,
)

View File

@@ -0,0 +1,50 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun TabIndicator(currentTabPosition: TabPosition) {
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(currentTabPosition)
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
)
}
@Composable
fun TabText(
text: String,
badgeCount: Int? = null,
isCurrentPage: Boolean,
) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = text,
color = if (isCurrentPage) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground,
)
if (badgeCount != null) {
Pill(
text = "$badgeCount",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 10.sp,
)
}
}
}

View File

@@ -2,31 +2,22 @@ package eu.kanade.presentation.library.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.PagerState
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.components.DownloadedOnlyModeBanner
import eu.kanade.presentation.components.IncognitoModeBanner
import eu.kanade.presentation.components.Pill
import eu.kanade.presentation.components.TabIndicator
import eu.kanade.presentation.components.TabText
import kotlinx.coroutines.launch
@Composable
@@ -46,13 +37,7 @@ fun LibraryTabs(
ScrollableTabRow(
selectedTabIndex = state.currentPage,
edgePadding = 0.dp,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(tabPositions[state.currentPage])
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
)
},
indicator = { TabIndicator(it[state.currentPage]) },
) {
categories.forEachIndexed { index, category ->
val count by if (showMangaCount) {
@@ -64,21 +49,7 @@ fun LibraryTabs(
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = category.visualName,
color = if (state.currentPage == index) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground,
)
if (count != null) {
Pill(
text = "$count",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 10.sp,
)
}
}
TabText(category.visualName, count, state.currentPage == index)
},
)
}