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.

(cherry picked from commit 34906a74253e7463ac23ea96496c59198884e0be)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt
This commit is contained in:
Ivan Iskandar 2022-07-09 23:37:49 +07:00 committed by Jobobby04
parent 4e29fd5b2a
commit d5aecaad21
5 changed files with 442 additions and 562 deletions

View File

@ -2,15 +2,11 @@ package eu.kanade.presentation.manga
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
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.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
@ -36,12 +32,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberTopAppBarScrollState
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
@ -49,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@ -65,13 +58,14 @@ import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.components.VerticalFastScroller
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.MangaChapterListItem
import eu.kanade.presentation.manga.components.MangaInfoHeader
import eu.kanade.presentation.manga.components.MangaInfoBox
import eu.kanade.presentation.manga.components.MangaInfoButtons
import eu.kanade.presentation.manga.components.MangaSmallAppBar
import eu.kanade.presentation.manga.components.MangaTopAppBar
import eu.kanade.presentation.manga.components.SearchMetadataChips
import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrollingUp
import eu.kanade.presentation.util.plus
@ -85,7 +79,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreenState
import eu.kanade.tachiyomi.util.lang.toRelativeString
import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource
import kotlinx.coroutines.runBlocking
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Date
@ -238,175 +231,214 @@ private fun MangaScreenSmallImpl(
onMultiDeleteClicked: (List<Chapter>) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec)
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 (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) }
SwipeRefresh(
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
onRefresh = onRefresh,
indicatorPadding = PaddingValues(
start = insetPadding.calculateStartPadding(layoutDirection),
top = with(LocalDensity.current) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection),
),
indicator = { s, trigger ->
SwipeRefreshIndicator(
state = s,
refreshTriggerDistance = trigger,
val chapters = remember(state) { state.processedChapters.toList() }
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
// SY -->
val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() }
// SY <--
val internalOnBackPressed = {
if (selected.isNotEmpty()) {
selected.clear()
} else {
onBackClicked()
}
}
BackHandler(onBack = internalOnBackPressed)
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,
// SY -->
showEditInfo = state.manga.favorite,
onEditInfoClicked = onEditInfoClicked,
showRecommends = state.showRecommendationsInOverflow,
onRecommendClicked = onRecommendClicked,
showMergeSettings = state.manga.source == MERGED_SOURCE_ID,
onMergedSettingsClicked = onMergedSettingsClicked,
// SY <--
actionModeCounter = selected.size,
onSelectAll = {
selected.clear()
selected.addAll(chapters)
},
onInvertSelection = {
val toSelect = chapters - selected
selected.clear()
selected.addAll(toSelect)
},
)
},
) {
val chapters = remember(state) { state.processedChapters.toList() }
val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
val internalOnBackPressed = {
if (selected.isNotEmpty()) {
selected.clear()
} else {
onBackClicked()
}
}
BackHandler(onBack = internalOnBackPressed)
Scaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.padding(insetPadding),
topBar = {
MangaTopAppBar(
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
.scrollable(
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
val newOffset =
(scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f)
consumed = newOffset - scrollBehavior.state.offset
scrollBehavior.state.offset = newOffset
}
consumed
},
orientation = Orientation.Vertical,
interactionSource = chapterListState.interactionSource as MutableInteractionSource,
),
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
description = state.manga.description,
tagsProvider = { state.manga.genre },
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,
onEditInfoClicked = onEditInfoClicked,
onRecommendClicked = onRecommendClicked,
showRecommendsInOverflow = state.showRecommendationsInOverflow,
showMergedSettings = state.manga.source == MERGED_SOURCE_ID,
onMergedSettingsClicked = onMergedSettingsClicked,
onMergeClicked = onMergeClicked,
doGlobalSearch = onSearch,
showMergeWithAnother = state.showMergeWithAnother,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
mangaMetadataHeader = getDescriptionComposable(
source = remember { state.source.getMainSource<MetadataSource<*, *>>() },
state = state,
openMetadataViewer = onMetadataViewerClicked,
search = { onSearch(it, false) },
),
searchMetadataChips = remember(state) { SearchMetadataChips(state.meta, state.source, state.manga.genre) },
scrollBehavior = scrollBehavior,
actionModeCounter = selected.size,
onSelectAll = {
selected.clear()
selected.addAll(chapters)
},
onInvertSelection = {
val toSelect = chapters - selected
selected.clear()
selected.addAll(toSelect)
},
onSmallAppBarHeightChanged = onTopBarHeightChanged,
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
)
}
},
) { contentPadding ->
val noTopContentPadding = PaddingValues(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding(),
) + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
val topPadding = contentPadding.calculateTopPadding()
SwipeRefresh(
state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
onRefresh = onRefresh,
indicatorPadding = contentPadding,
indicator = { s, trigger ->
SwipeRefreshIndicator(
state = s,
refreshTriggerDistance = trigger,
)
},
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(
listState = chapterListState,
thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit },
topContentPadding = withNavBarContentPadding.calculateTopPadding(),
endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
topContentPadding = topPadding,
endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
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,
// SY -->
onMergeClicked = onMergeClicked,
// SY <--
)
}
// SY -->
if (metadataSource != null) {
item(contentType = "metadata_info") {
metadataSource.DescriptionComposable(
state = state,
openMetadataViewer = onMetadataViewerClicked,
search = { onSearch(it, false) },
)
}
}
// SY <--
item(contentType = "desc") {
ExpandableMangaDescription(
defaultExpandState = state.isFromSource,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
SearchMetadataChips(state.meta, state.source, state.manga.genre)
},
// SY <--
)
}
// SY -->
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
item(contentType = "info_buttons") {
MangaInfoButtons(
showRecommendsButton = !state.showRecommendationsInOverflow,
showMergeWithAnotherButton = state.showMergeWithAnother,
onRecommendClicked = onRecommendClicked,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
)
}
}
// SY <--
item(contentType = "header") {
ChapterHeader(
chapterCount = chapters.size,
isChapterFiltered = state.manga.chaptersFiltered(),
onFilterButtonClicked = onFilterButtonClicked,
)
}
sharedChapterItems(
chapters = chapters,
state = state,
@ -462,6 +494,10 @@ fun MangaScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
// SY -->
val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() }
// SY <--
val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
SwipeRefresh(
@ -571,45 +607,66 @@ fun MangaScreenLargeImpl(
Row {
val withNavBarContentPadding = contentPadding +
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
MangaInfoHeader(
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
.padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
windowWidthSizeClass = WindowWidthSizeClass.Expanded,
appBarPadding = contentPadding.calculateTopPadding(),
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
description = state.manga.description,
tagsProvider = { state.manga.genre },
sourceName = remember { state.source.getNameForMangaInfo() },
isStubSource = remember { state.source is SourceManager.StubSource },
coverDataProvider = { state.manga },
favorite = state.manga.favorite,
status = state.manga.status,
trackingCount = state.trackingCount,
fromSource = state.isFromSource,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onMergeClicked = onMergeClicked,
onTagClicked = onTagClicked,
onEditCategory = onEditCategoryClicked,
onCoverClick = onCoverClicked,
doSearch = onSearch,
showRecommendsInOverflow = state.showRecommendationsInOverflow,
showMergeWithAnother = state.showMergeWithAnother,
onRecommendClicked = onRecommendClicked,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
mangaMetadataHeader = getDescriptionComposable(
source = remember { state.source.getMainSource<MetadataSource<*, *>>() },
) {
MangaInfoBox(
windowWidthSizeClass = windowWidthSizeClass,
appBarPadding = contentPadding.calculateTopPadding(),
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,
)
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onEditCategory = onEditCategoryClicked,
// SY -->
onMergeClicked = onMergeClicked,
// SY <--
)
// SY -->
metadataSource?.DescriptionComposable(
state = state,
openMetadataViewer = onMetadataViewerClicked,
search = { onSearch(it, false) },
),
searchMetadataChips = remember(state) { SearchMetadataChips(state.meta, state.source, state.manga.genre) },
)
)
// SY <--
ExpandableMangaDescription(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
SearchMetadataChips(state.meta, state.source, state.manga.genre)
},
// SY <--
)
// SY -->
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
MangaInfoButtons(
showRecommendsButton = !state.showRecommendationsInOverflow,
showMergeWithAnotherButton = state.showMergeWithAnother,
onRecommendClicked = onRecommendClicked,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
)
}
// SY <--
}
val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
VerticalFastScroller(
@ -727,7 +784,7 @@ private fun LazyListScope.sharedChapterItems(
}
}
val lastPageRead = remember(chapter.lastPageRead) {
chapter.lastPageRead.takeIf { !chapter.read && it > 0 }
chapter.lastPageRead.takeIf { /* SY --> */(!chapter.read || state.alwaysShowPageProgress)/* SY <-- */ && it > 0 }
}
val scanlator = remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } }
@ -840,19 +897,3 @@ private fun onChapterItemClick(
else -> onChapterClicked(chapterItem.chapter)
}
}
@Composable
@ReadOnlyComposable
@Stable
private fun getDescriptionComposable(
source: MetadataSource<*, *>?,
state: MangaScreenState.Success,
openMetadataViewer: () -> Unit,
search: (String) -> Unit,
): (@Composable () -> Unit)? {
return if (source != null) {
@Composable {
source.DescriptionComposable(state = state, openMetadataViewer = openMetadataViewer, search = search)
}
} else null
}

View File

@ -83,219 +83,221 @@ import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable
fun MangaInfoHeader(
fun MangaInfoBox(
modifier: Modifier = Modifier,
windowWidthSizeClass: WindowWidthSizeClass,
appBarPadding: Dp,
title: String,
author: String?,
artist: String?,
description: String?,
tagsProvider: () -> List<String>?,
sourceName: String,
isStubSource: Boolean,
coverDataProvider: () -> Manga,
favorite: Boolean,
status: Long,
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
) {
Box(modifier = modifier) {
// Backdrop
val backdropGradientColors = listOf(
Color.Transparent,
MaterialTheme.colorScheme.background,
)
AsyncImage(
model = coverDataProvider(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.matchParentSize()
.drawWithContent {
drawContent()
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 = 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,
)
}
}
}
}
@Composable
fun MangaActionRow(
modifier: Modifier = Modifier,
favorite: Boolean,
trackingCount: Int,
fromSource: Boolean,
onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?,
onMergeClicked: () -> Unit,
onTagClicked: (String) -> Unit,
onEditCategory: (() -> Unit)?,
onCoverClick: () -> Unit,
doSearch: (query: String, global: Boolean) -> Unit,
onRecommendClicked: () -> Unit,
showRecommendsInOverflow: Boolean,
showMergeWithAnother: Boolean,
onMergeWithAnotherClicked: () -> Unit,
mangaMetadataHeader: (@Composable () -> Unit)?,
// SY -->
onMergeClicked: () -> Unit,
// SY <--
) {
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(
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,
)
}
// SY -->
MangaActionButton(
title = stringResource(R.string.merge),
icon = Icons.Outlined.CallMerge,
color = defaultActionButtonColor,
onClick = onMergeClicked,
)
// SY <--
}
}
@Composable
fun ExpandableMangaDescription(
modifier: Modifier = Modifier,
defaultExpandState: Boolean,
description: String?,
tagsProvider: () -> List<String>?,
onTagClicked: (String) -> Unit,
// SY -->
searchMetadataChips: SearchMetadataChips?,
doSearch: (query: String, global: Boolean) -> Unit,
// SY <--
) {
val context = LocalContext.current
Column(modifier = modifier) {
Box {
// Backdrop
val backdropGradientColors = listOf(
Color.Transparent,
MaterialTheme.colorScheme.background,
)
AsyncImage(
model = coverDataProvider(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.matchParentSize()
.drawWithContent {
drawContent()
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,
)
}
}
val (expanded, onExpanded) = rememberSaveable {
mutableStateOf(defaultExpandState)
}
// Action buttons
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(
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,
)
}
MangaActionButton(
title = stringResource(R.string.merge),
icon = Icons.Outlined.CallMerge,
color = defaultActionButtonColor,
onClick = onMergeClicked,
)
val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
// SY --> Manga metadata
mangaMetadataHeader?.invoke()
// SY <--
// Expandable description-tags
Column {
val (expanded, onExpanded) = rememberSaveable {
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
}
val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary(
expandedDescription = desc,
shrunkDescription = trimmedDescription,
expanded = expanded,
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
.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
.padding(top = 8.dp)
.padding(vertical = 12.dp)
.animateContentSize(),
) {
if (expanded) {
if (searchMetadataChips != null) {
NamespaceTags(
tags = searchMetadataChips,
onClick = onTagClicked,
onLongClick = { doSearch(it, true) },
)
} else {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
mainAxisSpacing = 4.dp,
crossAxisSpacing = 8.dp,
) {
tags.forEach {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
onLongClick = { doSearch(it, true) },
)
}
}
}
.padding(vertical = 12.dp)
.animateContentSize(),
) {
if (expanded) {
// SY -->
if (searchMetadataChips != null) {
NamespaceTags(
tags = searchMetadataChips,
onClick = onTagClicked,
onLongClick = { doSearch(it, true) },
)
} else {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
// SY <--
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
mainAxisSpacing = 4.dp,
crossAxisSpacing = 8.dp,
) {
items(items = tags) {
tags.forEach {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
// SY -->
onLongClick = { doSearch(it, true) },
// SY <--
)
}
}
}
} else {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
items(items = tags) {
TagsChip(
text = it,
onClick = { onTagClicked(it) },
// SY -->
onLongClick = { doSearch(it, true) },
// SY <--
)
}
}
}
}
}
// SY -->
MangaInfoButtons(
showRecommendsButton = !showRecommendsInOverflow,
showMergeWithAnotherButton = showMergeWithAnother,
onRecommendClicked = onRecommendClicked,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
)
// SY <--
}
}

View File

@ -54,12 +54,14 @@ fun MangaSmallAppBar(
onDownloadClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?,
// SY -->
showEditInfo: Boolean,
onEditInfoClicked: () -> Unit,
showRecommends: Boolean,
onRecommendClicked: () -> Unit,
showMergeSettings: Boolean,
onMergedSettingsClicked: () -> Unit,
// SY <--
// For action mode
actionModeCounter: Int,
onSelectAll: () -> Unit,

View File

@ -1,167 +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)?,
onEditInfoClicked: () -> Unit,
onRecommendClicked: () -> Unit,
showRecommendsInOverflow: Boolean,
showMergedSettings: Boolean,
onMergedSettingsClicked: () -> Unit,
onMergeClicked: () -> Unit,
showMergeWithAnother: Boolean,
onMergeWithAnotherClicked: () -> Unit,
doGlobalSearch: (query: String, global: Boolean) -> Unit,
mangaMetadataHeader: (@Composable () -> Unit)?,
searchMetadataChips: SearchMetadataChips?,
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,
onMergeClicked = onMergeClicked,
onTagClicked = onTagClicked,
onEditCategory = onEditCategoryClicked,
onCoverClick = onCoverClick,
doSearch = doGlobalSearch,
showRecommendsInOverflow = showRecommendsInOverflow,
showMergeWithAnother = showMergeWithAnother,
onRecommendClicked = onRecommendClicked,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
mangaMetadataHeader = mangaMetadataHeader,
searchMetadataChips = searchMetadataChips,
)
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,
// SY -->
showEditInfo = favorite,
onEditInfoClicked = onEditInfoClicked,
showRecommends = showRecommendsInOverflow,
onRecommendClicked = onRecommendClicked,
showMergeSettings = showMergedSettings,
onMergedSettingsClicked = onMergedSettingsClicked,
// SY <--
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
}
}
}
}

View File

@ -306,6 +306,7 @@ class MangaPresenter(
mergedData = mergedData,
showRecommendationsInOverflow = preferences.recommendsInOverflow().get(),
showMergeWithAnother = smartSearched,
alwaysShowPageProgress = preferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
)
}
@ -1333,6 +1334,7 @@ sealed class MangaScreenState {
val mergedData: MergedMangaData?,
val showRecommendationsInOverflow: Boolean,
val showMergeWithAnother: Boolean,
val alwaysShowPageProgress: Boolean,
// SY <--
) : MangaScreenState() {