MangaScreen: Ditch the expanded app bar (#7470)

Animating the content padding that's used for the lazy list is heavy. A simple
fix to *just* offset the list is blocked by a Compose fling issue (b/179417109).

So I decided to go with the previous layout of this screen by putting everything
in the list. MangaInfoHeader is split into separate composables to avoid jank
when the item is being inflated.
This commit is contained in:
Ivan Iskandar 2022-07-09 23:37:49 +07:00 committed by GitHub
parent 86bacbe586
commit 34906a7425
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 337 additions and 462 deletions

View file

@ -2,15 +2,11 @@ package eu.kanade.presentation.manga
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@ -36,10 +32,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberTopAppBarScrollState
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
@ -47,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@ -63,12 +58,12 @@ import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefreshIndicator import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.components.VerticalFastScroller import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
import eu.kanade.presentation.manga.components.MangaActionRow
import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaBottomActionMenu
import eu.kanade.presentation.manga.components.MangaChapterListItem import eu.kanade.presentation.manga.components.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoHeader import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaSmallAppBar import eu.kanade.presentation.manga.components.MangaSmallAppBar
import eu.kanade.presentation.manga.components.MangaTopAppBar
import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior
import eu.kanade.presentation.util.isScrolledToEnd import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrollingUp import eu.kanade.presentation.util.isScrollingUp
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
@ -79,7 +74,6 @@ import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.manga.ChapterItem import eu.kanade.tachiyomi.ui.manga.ChapterItem
import eu.kanade.tachiyomi.ui.manga.MangaScreenState import eu.kanade.tachiyomi.ui.manga.MangaScreenState
import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.lang.toRelativeString
import kotlinx.coroutines.runBlocking
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
import java.util.Date import java.util.Date
@ -208,160 +202,169 @@ private fun MangaScreenSmallImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit, onMultiDeleteClicked: (List<Chapter>) -> Unit,
) { ) {
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec)
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
SideEffect {
if (chapterListState.firstVisibleItemIndex > 0 || chapterListState.firstVisibleItemScrollOffset > 0) {
// Should go here after a configuration change
// Safe to say that the app bar is fully scrolled
scrollBehavior.state.offset = scrollBehavior.state.offsetLimit
}
}
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) } val chapters = remember(state) { state.processedChapters.toList() }
SwipeRefresh( val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter), val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
onRefresh = onRefresh,
indicatorPadding = PaddingValues( val internalOnBackPressed = {
start = insetPadding.calculateStartPadding(layoutDirection), if (selected.isNotEmpty()) {
top = with(LocalDensity.current) { topBarHeight.toDp() }, selected.clear()
end = insetPadding.calculateEndPadding(layoutDirection), } else {
), onBackClicked()
indicator = { s, trigger -> }
SwipeRefreshIndicator( }
state = s, BackHandler(onBack = internalOnBackPressed)
refreshTriggerDistance = trigger,
Scaffold(
modifier = Modifier
.padding(insetPadding),
topBar = {
val firstVisibleItemIndex by remember {
derivedStateOf { chapterListState.firstVisibleItemIndex }
}
val firstVisibleItemScrollOffset by remember {
derivedStateOf { chapterListState.firstVisibleItemScrollOffset }
}
val animatedTitleAlpha by animateFloatAsState(
if (firstVisibleItemIndex > 0) 1f else 0f,
)
val animatedBgAlpha by animateFloatAsState(
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
)
MangaSmallAppBar(
title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
incognitoMode = state.isIncognitoMode,
downloadedOnlyMode = state.isDownloadedOnlyMode,
onBackClicked = onBackClicked,
onShareClicked = onShareClicked,
onDownloadClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
actionModeCounter = selected.size,
onSelectAll = {
selected.clear()
selected.addAll(chapters)
},
onInvertSelection = {
val toSelect = chapters - selected
selected.clear()
selected.addAll(toSelect)
},
) )
}, },
) { bottomBar = {
val chapters = remember(state) { state.processedChapters.toList() } SharedMangaBottomActionMenu(
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() } selected = selected,
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
val internalOnBackPressed = { onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
if (selected.isNotEmpty()) { onDownloadChapter = onDownloadChapter,
selected.clear() onMultiDeleteClicked = onMultiDeleteClicked,
} else { fillFraction = 1f,
onBackClicked() )
} },
} snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
BackHandler(onBack = internalOnBackPressed) floatingActionButton = {
AnimatedVisibility(
Scaffold( visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
modifier = Modifier enter = fadeIn(),
.nestedScroll(scrollBehavior.nestedScrollConnection) exit = fadeOut(),
.padding(insetPadding), ) {
topBar = { ExtendedFloatingActionButton(
MangaTopAppBar( text = {
val id = if (chapters.any { it.chapter.read }) {
R.string.action_resume
} else {
R.string.action_start
}
Text(text = stringResource(id))
},
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
modifier = Modifier modifier = Modifier
.scrollable( .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
state = rememberScrollableState { )
var consumed = runBlocking { chapterListState.scrollBy(-it) } * -1 }
if (consumed == 0f) { },
// Pass scroll to app bar if we're on the top of the list ) { contentPadding ->
val newOffset = val noTopContentPadding = PaddingValues(
(scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f) start = contentPadding.calculateStartPadding(layoutDirection),
consumed = newOffset - scrollBehavior.state.offset end = contentPadding.calculateEndPadding(layoutDirection),
scrollBehavior.state.offset = newOffset bottom = contentPadding.calculateBottomPadding(),
} ) + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
consumed val topPadding = contentPadding.calculateTopPadding()
},
orientation = Orientation.Vertical, SwipeRefresh(
interactionSource = chapterListState.interactionSource as MutableInteractionSource, state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
), onRefresh = onRefresh,
title = state.manga.title, indicatorPadding = contentPadding,
author = state.manga.author, indicator = { s, trigger ->
artist = state.manga.artist, SwipeRefreshIndicator(
description = state.manga.description, state = s,
tagsProvider = { state.manga.genre }, refreshTriggerDistance = trigger,
coverDataProvider = { state.manga },
sourceName = remember { state.source.getNameForMangaInfo() },
isStubSource = remember { state.source is SourceManager.StubSource },
favorite = state.manga.favorite,
status = state.manga.status,
trackingCount = state.trackingCount,
chapterCount = chapters.size,
chapterFiltered = state.manga.chaptersFiltered(),
incognitoMode = state.isIncognitoMode,
downloadedOnlyMode = state.isDownloadedOnlyMode,
fromSource = state.isFromSource,
onBackClicked = internalOnBackPressed,
onCoverClick = onCoverClicked,
onTagClicked = onTagClicked,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onFilterButtonClicked = onFilterButtonClicked,
onShareClicked = onShareClicked,
onDownloadClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
doGlobalSearch = onSearch,
scrollBehavior = scrollBehavior,
actionModeCounter = selected.size,
onSelectAll = {
selected.clear()
selected.addAll(chapters)
},
onInvertSelection = {
val toSelect = chapters - selected
selected.clear()
selected.addAll(toSelect)
},
onSmallAppBarHeightChanged = onTopBarHeightChanged,
) )
}, },
bottomBar = { ) {
SharedMangaBottomActionMenu(
selected = selected,
onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
onDownloadChapter = onDownloadChapter,
onMultiDeleteClicked = onMultiDeleteClicked,
fillFraction = 1f,
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
AnimatedVisibility(
visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val id = if (chapters.any { it.chapter.read }) {
R.string.action_resume
} else {
R.string.action_start
}
Text(text = stringResource(id))
},
icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
modifier = Modifier
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
)
}
},
) { contentPadding ->
val withNavBarContentPadding = contentPadding +
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
VerticalFastScroller( VerticalFastScroller(
listState = chapterListState, listState = chapterListState,
thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit }, topContentPadding = topPadding,
topContentPadding = withNavBarContentPadding.calculateTopPadding(), endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection),
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
state = chapterListState, state = chapterListState,
contentPadding = withNavBarContentPadding, contentPadding = noTopContentPadding,
) { ) {
item(contentType = "info_box") {
MangaInfoBox(
windowWidthSizeClass = WindowWidthSizeClass.Compact,
appBarPadding = topPadding,
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo() },
isStubSource = remember { state.source is SourceManager.StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked,
doSearch = onSearch,
)
}
item(contentType = "action_row") {
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onEditCategory = onEditCategoryClicked,
)
}
item(contentType = "desc") {
ExpandableMangaDescription(
defaultExpandState = state.isFromSource,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
)
}
item(contentType = "header") {
ChapterHeader(
chapterCount = chapters.size,
isChapterFiltered = state.manga.chaptersFiltered(),
onFilterButtonClicked = onFilterButtonClicked,
)
}
sharedChapterItems( sharedChapterItems(
chapters = chapters, chapters = chapters,
state = state, state = state,
@ -514,33 +517,40 @@ fun MangaScreenLargeImpl(
Row { Row {
val withNavBarContentPadding = contentPadding + val withNavBarContentPadding = contentPadding +
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
MangaInfoHeader( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(bottom = withNavBarContentPadding.calculateBottomPadding()), .padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
windowWidthSizeClass = WindowWidthSizeClass.Expanded, ) {
appBarPadding = contentPadding.calculateTopPadding(), MangaInfoBox(
title = state.manga.title, windowWidthSizeClass = windowWidthSizeClass,
author = state.manga.author, appBarPadding = contentPadding.calculateTopPadding(),
artist = state.manga.artist, title = state.manga.title,
description = state.manga.description, author = state.manga.author,
tagsProvider = { state.manga.genre }, artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo() }, sourceName = remember { state.source.getNameForMangaInfo() },
isStubSource = remember { state.source is SourceManager.StubSource }, isStubSource = remember { state.source is SourceManager.StubSource },
coverDataProvider = { state.manga }, coverDataProvider = { state.manga },
favorite = state.manga.favorite, status = state.manga.status,
status = state.manga.status, onCoverClick = onCoverClicked,
trackingCount = state.trackingCount, doSearch = onSearch,
fromSource = state.isFromSource, )
onAddToLibraryClicked = onAddToLibraryClicked, MangaActionRow(
onWebViewClicked = onWebViewClicked, favorite = state.manga.favorite,
onTrackingClicked = onTrackingClicked, trackingCount = state.trackingCount,
onTagClicked = onTagClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onEditCategory = onEditCategoryClicked, onWebViewClicked = onWebViewClicked,
onCoverClick = onCoverClicked, onTrackingClicked = onTrackingClicked,
doSearch = onSearch, onEditCategory = onEditCategoryClicked,
) )
ExpandableMangaDescription(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
)
}
val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
VerticalFastScroller( VerticalFastScroller(

View file

@ -85,179 +85,185 @@ import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable @Composable
fun MangaInfoHeader( fun MangaInfoBox(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
windowWidthSizeClass: WindowWidthSizeClass, windowWidthSizeClass: WindowWidthSizeClass,
appBarPadding: Dp, appBarPadding: Dp,
title: String, title: String,
author: String?, author: String?,
artist: String?, artist: String?,
description: String?,
tagsProvider: () -> List<String>?,
sourceName: String, sourceName: String,
isStubSource: Boolean, isStubSource: Boolean,
coverDataProvider: () -> Manga, coverDataProvider: () -> Manga,
favorite: Boolean,
status: Long, status: Long,
trackingCount: Int,
fromSource: Boolean,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onTagClicked: (String) -> Unit,
onEditCategory: (() -> Unit)?,
onCoverClick: () -> Unit, onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
) { ) {
val context = LocalContext.current Box(modifier = modifier) {
Column(modifier = modifier) { // Backdrop
Box { val backdropGradientColors = listOf(
// Backdrop Color.Transparent,
val backdropGradientColors = listOf( MaterialTheme.colorScheme.background,
Color.Transparent, )
MaterialTheme.colorScheme.background, AsyncImage(
) model = coverDataProvider(),
AsyncImage( contentDescription = null,
model = coverDataProvider(), contentScale = ContentScale.Crop,
contentDescription = null, modifier = Modifier
contentScale = ContentScale.Crop, .matchParentSize()
modifier = Modifier .drawWithContent {
.matchParentSize() drawContent()
.drawWithContent { drawRect(
drawContent() brush = Brush.verticalGradient(colors = backdropGradientColors),
drawRect(
brush = Brush.verticalGradient(colors = backdropGradientColors),
)
}
.alpha(.2f),
)
// Manga & source info
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
MangaAndSourceTitlesSmall(
appBarPadding = appBarPadding,
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = context,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName,
isStubSource = isStubSource,
)
} else {
MangaAndSourceTitlesLarge(
appBarPadding = appBarPadding,
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = context,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName,
isStubSource = isStubSource,
) )
} }
.alpha(.2f),
)
// Manga & source info
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
MangaAndSourceTitlesSmall(
appBarPadding = appBarPadding,
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = LocalContext.current,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName,
isStubSource = isStubSource,
)
} else {
MangaAndSourceTitlesLarge(
appBarPadding = appBarPadding,
coverDataProvider = coverDataProvider,
onCoverClick = onCoverClick,
title = title,
context = LocalContext.current,
doSearch = doSearch,
author = author,
artist = artist,
status = status,
sourceName = sourceName,
isStubSource = isStubSource,
)
} }
} }
}
}
// Action buttons @Composable
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { fun MangaActionRow(
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) modifier: Modifier = Modifier,
favorite: Boolean,
trackingCount: Int,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onEditCategory: (() -> Unit)?,
) {
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
MangaActionButton(
title = if (favorite) {
stringResource(R.string.in_library)
} else {
stringResource(R.string.add_to_library)
},
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onAddToLibraryClicked,
onLongClick = onEditCategory,
)
if (onTrackingClicked != null) {
MangaActionButton( MangaActionButton(
title = if (favorite) { title = if (trackingCount == 0) {
stringResource(R.string.in_library) stringResource(R.string.manga_tracking_tab)
} else { } else {
stringResource(R.string.add_to_library) quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
}, },
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor, color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
onClick = onAddToLibraryClicked, onClick = onTrackingClicked,
onLongClick = onEditCategory,
) )
if (onTrackingClicked != null) {
MangaActionButton(
title = if (trackingCount == 0) {
stringResource(R.string.manga_tracking_tab)
} else {
quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
},
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
onClick = onTrackingClicked,
)
}
if (onWebViewClicked != null) {
MangaActionButton(
title = stringResource(R.string.action_web_view),
icon = Icons.Default.Public,
color = defaultActionButtonColor,
onClick = onWebViewClicked,
)
}
} }
if (onWebViewClicked != null) {
MangaActionButton(
title = stringResource(R.string.action_web_view),
icon = Icons.Default.Public,
color = defaultActionButtonColor,
onClick = onWebViewClicked,
)
}
}
}
// Expandable description-tags @Composable
Column { fun ExpandableMangaDescription(
val (expanded, onExpanded) = rememberSaveable { modifier: Modifier = Modifier,
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact) defaultExpandState: Boolean,
} description: String?,
val desc = tagsProvider: () -> List<String>?,
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) onTagClicked: (String) -> Unit,
val trimmedDescription = remember(desc) { ) {
desc val context = LocalContext.current
.replace(whitespaceLineRegex, "\n") Column(modifier = modifier) {
.trimEnd() val (expanded, onExpanded) = rememberSaveable {
} mutableStateOf(defaultExpandState)
MangaSummary( }
expandedDescription = desc, val desc =
shrunkDescription = trimmedDescription, description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
expanded = expanded, val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary(
expandedDescription = desc,
shrunkDescription = trimmedDescription,
expanded = expanded,
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
.clickableNoIndication(
onLongClick = { context.copyToClipboard(desc, desc) },
onClick = { onExpanded(!expanded) },
),
)
val tags = tagsProvider()
if (!tags.isNullOrEmpty()) {
Box(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp) .padding(top = 8.dp)
.padding(horizontal = 16.dp) .padding(vertical = 12.dp)
.clickableNoIndication( .animateContentSize(),
onLongClick = { context.copyToClipboard(desc, desc) }, ) {
onClick = { onExpanded(!expanded) }, if (expanded) {
), FlowRow(
) modifier = Modifier.padding(horizontal = 16.dp),
val tags = tagsProvider() mainAxisSpacing = 4.dp,
if (!tags.isNullOrEmpty()) { crossAxisSpacing = 8.dp,
Box( ) {
modifier = Modifier tags.forEach {
.padding(top = 8.dp) TagsChip(
.padding(vertical = 12.dp) text = it,
.animateContentSize(), onClick = { onTagClicked(it) },
) { )
if (expanded) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
mainAxisSpacing = 4.dp,
crossAxisSpacing = 8.dp,
) {
tags.forEach {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
)
}
} }
} else { }
LazyRow( } else {
contentPadding = PaddingValues(horizontal = 16.dp), LazyRow(
horizontalArrangement = Arrangement.spacedBy(4.dp), contentPadding = PaddingValues(horizontal = 16.dp),
) { horizontalArrangement = Arrangement.spacedBy(4.dp),
items(items = tags) { ) {
TagsChip( items(items = tags) {
text = it, TagsChip(
onClick = { onTagClicked(it) }, text = it,
) onClick = { onTagClicked(it) },
} )
} }
} }
} }

View file

@ -1,141 +0,0 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.manga.DownloadAction
import kotlin.math.roundToInt
@Composable
fun MangaTopAppBar(
modifier: Modifier = Modifier,
title: String,
author: String?,
artist: String?,
description: String?,
tagsProvider: () -> List<String>?,
coverDataProvider: () -> Manga,
sourceName: String,
isStubSource: Boolean,
favorite: Boolean,
status: Long,
trackingCount: Int,
chapterCount: Int?,
chapterFiltered: Boolean,
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
fromSource: Boolean,
onBackClicked: () -> Unit,
onCoverClick: () -> Unit,
onTagClicked: (String) -> Unit,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onFilterButtonClicked: () -> Unit,
onShareClicked: (() -> Unit)?,
onDownloadClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
doGlobalSearch: (query: String, global: Boolean) -> Unit,
scrollBehavior: TopAppBarScrollBehavior?,
// For action mode
actionModeCounter: Int,
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
onSmallAppBarHeightChanged: (Int) -> Unit,
) {
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
Layout(
modifier = modifier,
content = {
val (smallHeightPx, onSmallHeightPxChanged) = remember { mutableStateOf(0) }
Column(modifier = Modifier.layoutId("mangaInfo")) {
MangaInfoHeader(
windowWidthSizeClass = WindowWidthSizeClass.Compact,
appBarPadding = with(LocalDensity.current) { smallHeightPx.toDp() },
title = title,
author = author,
artist = artist,
description = description,
tagsProvider = tagsProvider,
sourceName = sourceName,
isStubSource = isStubSource,
coverDataProvider = coverDataProvider,
favorite = favorite,
status = status,
trackingCount = trackingCount,
fromSource = fromSource,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onTagClicked = onTagClicked,
onEditCategory = onEditCategoryClicked,
onCoverClick = onCoverClick,
doSearch = doGlobalSearch,
)
ChapterHeader(
chapterCount = chapterCount,
isChapterFiltered = chapterFiltered,
onFilterButtonClicked = onFilterButtonClicked,
)
}
MangaSmallAppBar(
modifier = Modifier
.layoutId("topBar")
.onSizeChanged {
onSmallHeightPxChanged(it.height)
onSmallAppBarHeightChanged(it.height)
},
title = title,
titleAlphaProvider = { if (actionModeCounter == 0) scrollPercentageProvider() else 1f },
incognitoMode = incognitoMode,
downloadedOnlyMode = downloadedOnlyMode,
onBackClicked = onBackClicked,
onShareClicked = onShareClicked,
onDownloadClicked = onDownloadClicked,
onEditCategoryClicked = onEditCategoryClicked,
onMigrateClicked = onMigrateClicked,
actionModeCounter = actionModeCounter,
onSelectAll = onSelectAll,
onInvertSelection = onInvertSelection,
)
},
) { measurables, constraints ->
val mangaInfoPlaceable = measurables
.first { it.layoutId == "mangaInfo" }
.measure(constraints.copy(maxHeight = Constraints.Infinity))
val topBarPlaceable = measurables
.first { it.layoutId == "topBar" }
.measure(constraints)
val mangaInfoHeight = mangaInfoPlaceable.height
val topBarHeight = topBarPlaceable.height
val mangaInfoSansTopBarHeightPx = mangaInfoHeight - topBarHeight
val layoutHeight = topBarHeight +
(mangaInfoSansTopBarHeightPx * inverseScrollPercentageProvider()).roundToInt()
layout(constraints.maxWidth, layoutHeight) {
val mangaInfoY = (-mangaInfoSansTopBarHeightPx * scrollPercentageProvider()).roundToInt()
mangaInfoPlaceable.place(0, mangaInfoY)
topBarPlaceable.place(0, 0)
// Update offset limit
val offsetLimit = -mangaInfoSansTopBarHeightPx.toFloat()
if (scrollBehavior?.state?.offsetLimit != offsetLimit) {
scrollBehavior?.state?.offsetLimit = offsetLimit
}
}
}
}