diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 62432606da..95beb41cca 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -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) -> Unit, ) { val layoutDirection = LocalLayoutDirection.current - val decayAnimationSpec = rememberSplineBasedDecay() - 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().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().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( diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 01aa0559fd..82dc90c490 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -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?, 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?, + 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) }, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt deleted file mode 100644 index 9af664d672..0000000000 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt +++ /dev/null @@ -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?, - 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 - } - } - } -}