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.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,12 +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.ReadOnlyComposable import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue
import androidx.compose.runtime.Stable
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
@ -49,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
@ -65,13 +58,14 @@ 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.MangaInfoButtons
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.manga.components.SearchMetadataChips import eu.kanade.presentation.manga.components.SearchMetadataChips
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
@ -85,7 +79,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreenState
import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.lang.toRelativeString
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
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
@ -238,175 +231,214 @@ 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, // SY -->
indicatorPadding = PaddingValues( val metadataSource = remember(state.source.id) { state.source.getMainSource<MetadataSource<*, *>>() }
start = insetPadding.calculateStartPadding(layoutDirection), // SY <--
top = with(LocalDensity.current) { topBarHeight.toDp() },
end = insetPadding.calculateEndPadding(layoutDirection), val internalOnBackPressed = {
), if (selected.isNotEmpty()) {
indicator = { s, trigger -> selected.clear()
SwipeRefreshIndicator( } else {
state = s, onBackClicked()
refreshTriggerDistance = trigger, }
}
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)
},
) )
}, },
) { 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,
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,
) )
}, },
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,
// 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( sharedChapterItems(
chapters = chapters, chapters = chapters,
state = state, state = state,
@ -462,6 +494,10 @@ fun MangaScreenLargeImpl(
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.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 insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) } val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) }
SwipeRefresh( SwipeRefresh(
@ -571,45 +607,66 @@ 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,
onMergeClicked = onMergeClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onTagClicked = onTagClicked, onWebViewClicked = onWebViewClicked,
onEditCategory = onEditCategoryClicked, onTrackingClicked = onTrackingClicked,
onCoverClick = onCoverClicked, onEditCategory = onEditCategoryClicked,
doSearch = onSearch, // SY -->
showRecommendsInOverflow = state.showRecommendationsInOverflow, onMergeClicked = onMergeClicked,
showMergeWithAnother = state.showMergeWithAnother, // SY <--
onRecommendClicked = onRecommendClicked, )
onMergeWithAnotherClicked = onMergeWithAnotherClicked, // SY -->
mangaMetadataHeader = getDescriptionComposable( metadataSource?.DescriptionComposable(
source = remember { state.source.getMainSource<MetadataSource<*, *>>() },
state = state, state = state,
openMetadataViewer = onMetadataViewerClicked, openMetadataViewer = onMetadataViewerClicked,
search = { onSearch(it, false) }, 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 val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
VerticalFastScroller( VerticalFastScroller(
@ -727,7 +784,7 @@ private fun LazyListScope.sharedChapterItems(
} }
} }
val lastPageRead = remember(chapter.lastPageRead) { 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() } } val scanlator = remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } }
@ -840,19 +897,3 @@ private fun onChapterItemClick(
else -> onChapterClicked(chapterItem.chapter) 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)) 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,
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, trackingCount: Int,
fromSource: Boolean,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?,
onMergeClicked: () -> Unit,
onTagClicked: (String) -> Unit,
onEditCategory: (() -> Unit)?, onEditCategory: (() -> Unit)?,
onCoverClick: () -> Unit, // SY -->
doSearch: (query: String, global: Boolean) -> Unit, onMergeClicked: () -> Unit,
onRecommendClicked: () -> Unit, // SY <--
showRecommendsInOverflow: Boolean, ) {
showMergeWithAnother: Boolean, Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
onMergeWithAnotherClicked: () -> Unit, val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
mangaMetadataHeader: (@Composable () -> Unit)?, 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?, searchMetadataChips: SearchMetadataChips?,
doSearch: (query: String, global: Boolean) -> Unit,
// SY <--
) { ) {
val context = LocalContext.current val context = LocalContext.current
Column(modifier = modifier) { Column(modifier = modifier) {
Box { val (expanded, onExpanded) = rememberSaveable {
// Backdrop mutableStateOf(defaultExpandState)
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 desc =
// Action buttons description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { val trimmedDescription = remember(desc) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) desc
MangaActionButton( .replace(whitespaceLineRegex, "\n")
title = if (favorite) { .trimEnd()
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,
)
} }
MangaSummary(
// SY --> Manga metadata expandedDescription = desc,
mangaMetadataHeader?.invoke() shrunkDescription = trimmedDescription,
// SY <-- expanded = expanded,
modifier = Modifier
// Expandable description-tags .padding(top = 8.dp)
Column { .padding(horizontal = 16.dp)
val (expanded, onExpanded) = rememberSaveable { .clickableNoIndication(
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact) onLongClick = { context.copyToClipboard(desc, desc) },
} onClick = { onExpanded(!expanded) },
val desc = ),
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) )
val trimmedDescription = remember(desc) { val tags = tagsProvider()
desc if (!tags.isNullOrEmpty()) {
.replace(whitespaceLineRegex, "\n") Box(
.trimEnd()
}
MangaSummary(
expandedDescription = desc,
shrunkDescription = trimmedDescription,
expanded = expanded,
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) {
), // SY -->
) if (searchMetadataChips != null) {
val tags = tagsProvider() NamespaceTags(
if (!tags.isNullOrEmpty()) { tags = searchMetadataChips,
Box( onClick = onTagClicked,
modifier = Modifier onLongClick = { doSearch(it, true) },
.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) },
)
}
}
}
} else { } else {
LazyRow( // SY <--
contentPadding = PaddingValues(horizontal = 16.dp), FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(horizontal = 16.dp),
mainAxisSpacing = 4.dp,
crossAxisSpacing = 8.dp,
) { ) {
items(items = tags) { tags.forEach {
TagsChip( TagsChip(
text = it, text = it,
onClick = { onTagClicked(it) }, onClick = { onTagClicked(it) },
// SY -->
onLongClick = { doSearch(it, true) }, 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)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
// SY -->
showEditInfo: Boolean, showEditInfo: Boolean,
onEditInfoClicked: () -> Unit, onEditInfoClicked: () -> Unit,
showRecommends: Boolean, showRecommends: Boolean,
onRecommendClicked: () -> Unit, onRecommendClicked: () -> Unit,
showMergeSettings: Boolean, showMergeSettings: Boolean,
onMergedSettingsClicked: () -> Unit, onMergedSettingsClicked: () -> Unit,
// SY <--
// For action mode // For action mode
actionModeCounter: Int, actionModeCounter: Int,
onSelectAll: () -> Unit, 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, mergedData = mergedData,
showRecommendationsInOverflow = preferences.recommendsInOverflow().get(), showRecommendationsInOverflow = preferences.recommendsInOverflow().get(),
showMergeWithAnother = smartSearched, showMergeWithAnother = smartSearched,
alwaysShowPageProgress = preferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
) )
} }
@ -1333,6 +1334,7 @@ sealed class MangaScreenState {
val mergedData: MergedMangaData?, val mergedData: MergedMangaData?,
val showRecommendationsInOverflow: Boolean, val showRecommendationsInOverflow: Boolean,
val showMergeWithAnother: Boolean, val showMergeWithAnother: Boolean,
val alwaysShowPageProgress: Boolean,
// SY <-- // SY <--
) : MangaScreenState() { ) : MangaScreenState() {