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:
parent
86bacbe586
commit
34906a7425
3 changed files with 337 additions and 462 deletions
|
@ -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,10 +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.SideEffect
|
||||
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
|
||||
|
@ -47,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
|
||||
|
@ -63,12 +58,12 @@ 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.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.isScrollingUp
|
||||
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.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Date
|
||||
|
@ -208,160 +202,169 @@ 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
|
||||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
.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,
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
chapters = chapters,
|
||||
state = state,
|
||||
|
@ -514,33 +517,40 @@ 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,
|
||||
onTagClicked = onTagClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
onCoverClick = onCoverClicked,
|
||||
doSearch = onSearch,
|
||||
)
|
||||
) {
|
||||
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,
|
||||
)
|
||||
ExpandableMangaDescription(
|
||||
defaultExpandState = true,
|
||||
description = state.manga.description,
|
||||
tagsProvider = { state.manga.genre },
|
||||
onTagClicked = onTagClicked,
|
||||
)
|
||||
}
|
||||
|
||||
val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
|
||||
VerticalFastScroller(
|
||||
|
|
|
@ -85,179 +85,185 @@ 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,
|
||||
trackingCount: Int,
|
||||
fromSource: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
onCoverClick: () -> Unit,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
) {
|
||||
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,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
@Composable
|
||||
fun MangaActionRow(
|
||||
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(
|
||||
title = if (favorite) {
|
||||
stringResource(R.string.in_library)
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(R.string.manga_tracking_tab)
|
||||
} 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,
|
||||
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
|
||||
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||
onClick = onTrackingClicked,
|
||||
)
|
||||
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
|
||||
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,
|
||||
@Composable
|
||||
fun ExpandableMangaDescription(
|
||||
modifier: Modifier = Modifier,
|
||||
defaultExpandState: Boolean,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier = modifier) {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
mutableStateOf(defaultExpandState)
|
||||
}
|
||||
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,
|
||||
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) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
mainAxisSpacing = 4.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
.padding(vertical = 12.dp)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
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(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue