From d5aecaad211c60f33720ed8e1b7a3acea56d51e7 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:37:49 +0700 Subject: [PATCH] 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 --- .../kanade/presentation/manga/MangaScreen.kt | 477 ++++++++++-------- .../manga/components/MangaInfoHeader.kt | 356 ++++++------- .../manga/components/MangaSmallAppBar.kt | 2 + .../manga/components/MangaTopAppBar.kt | 167 ------ .../tachiyomi/ui/manga/MangaPresenter.kt | 2 + 5 files changed, 442 insertions(+), 562 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt 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 9a3c7271b..4e6b8e107 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,12 +32,10 @@ import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.rememberTopAppBarScrollState import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateList @@ -49,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -65,13 +58,14 @@ import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.SwipeRefreshIndicator import eu.kanade.presentation.components.VerticalFastScroller import eu.kanade.presentation.manga.components.ChapterHeader +import eu.kanade.presentation.manga.components.ExpandableMangaDescription +import eu.kanade.presentation.manga.components.MangaActionRow import eu.kanade.presentation.manga.components.MangaBottomActionMenu import eu.kanade.presentation.manga.components.MangaChapterListItem -import eu.kanade.presentation.manga.components.MangaInfoHeader +import eu.kanade.presentation.manga.components.MangaInfoBox +import eu.kanade.presentation.manga.components.MangaInfoButtons import eu.kanade.presentation.manga.components.MangaSmallAppBar -import eu.kanade.presentation.manga.components.MangaTopAppBar import eu.kanade.presentation.manga.components.SearchMetadataChips -import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior import eu.kanade.presentation.util.isScrolledToEnd import eu.kanade.presentation.util.isScrollingUp import eu.kanade.presentation.util.plus @@ -85,7 +79,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaScreenState import eu.kanade.tachiyomi.util.lang.toRelativeString import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource -import kotlinx.coroutines.runBlocking import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.Date @@ -238,175 +231,214 @@ private fun MangaScreenSmallImpl( onMultiDeleteClicked: (List) -> 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 + // SY --> + val metadataSource = remember(state.source.id) { state.source.getMainSource>() } + // SY <-- + + val internalOnBackPressed = { + if (selected.isNotEmpty()) { + selected.clear() + } else { + onBackClicked() + } + } + BackHandler(onBack = internalOnBackPressed) + + Scaffold( + modifier = Modifier + .padding(insetPadding), + topBar = { + val firstVisibleItemIndex by remember { + derivedStateOf { chapterListState.firstVisibleItemIndex } + } + val firstVisibleItemScrollOffset by remember { + derivedStateOf { chapterListState.firstVisibleItemScrollOffset } + } + val animatedTitleAlpha by animateFloatAsState( + if (firstVisibleItemIndex > 0) 1f else 0f, + ) + val animatedBgAlpha by animateFloatAsState( + if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f, + ) + MangaSmallAppBar( + title = state.manga.title, + titleAlphaProvider = { animatedTitleAlpha }, + backgroundAlphaProvider = { animatedBgAlpha }, + incognitoMode = state.isIncognitoMode, + downloadedOnlyMode = state.isDownloadedOnlyMode, + onBackClicked = onBackClicked, + onShareClicked = onShareClicked, + onDownloadClicked = onDownloadActionClicked, + onEditCategoryClicked = onEditCategoryClicked, + onMigrateClicked = onMigrateClicked, + // SY --> + showEditInfo = state.manga.favorite, + onEditInfoClicked = onEditInfoClicked, + showRecommends = state.showRecommendationsInOverflow, + onRecommendClicked = onRecommendClicked, + showMergeSettings = state.manga.source == MERGED_SOURCE_ID, + onMergedSettingsClicked = onMergedSettingsClicked, + // SY <-- + actionModeCounter = selected.size, + onSelectAll = { + selected.clear() + selected.addAll(chapters) + }, + onInvertSelection = { + val toSelect = chapters - selected + selected.clear() + selected.addAll(toSelect) + }, ) }, - ) { - val chapters = remember(state) { state.processedChapters.toList() } - val selected = remember(chapters) { emptyList().toMutableStateList() } - val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list - - val internalOnBackPressed = { - if (selected.isNotEmpty()) { - selected.clear() - } else { - onBackClicked() - } - } - BackHandler(onBack = internalOnBackPressed) - - Scaffold( - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .padding(insetPadding), - topBar = { - MangaTopAppBar( + bottomBar = { + SharedMangaBottomActionMenu( + selected = selected, + onMultiBookmarkClicked = onMultiBookmarkClicked, + onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, + onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, + onDownloadChapter = onDownloadChapter, + onMultiDeleteClicked = onMultiDeleteClicked, + fillFraction = 1f, + ) + }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + floatingActionButton = { + AnimatedVisibility( + visible = chapters.any { !it.chapter.read } && selected.isEmpty(), + enter = fadeIn(), + exit = fadeOut(), + ) { + ExtendedFloatingActionButton( + text = { + val id = if (chapters.any { it.chapter.read }) { + R.string.action_resume + } else { + R.string.action_start + } + Text(text = stringResource(id)) + }, + icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) }, + onClick = onContinueReading, + expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), modifier = Modifier - .scrollable( - state = rememberScrollableState { - var consumed = runBlocking { chapterListState.scrollBy(-it) } * -1 - if (consumed == 0f) { - // Pass scroll to app bar if we're on the top of the list - val newOffset = - (scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f) - consumed = newOffset - scrollBehavior.state.offset - scrollBehavior.state.offset = newOffset - } - consumed - }, - orientation = Orientation.Vertical, - interactionSource = chapterListState.interactionSource as MutableInteractionSource, - ), - title = state.manga.title, - author = state.manga.author, - artist = state.manga.artist, - description = state.manga.description, - tagsProvider = { state.manga.genre }, - coverDataProvider = { state.manga }, - sourceName = remember { state.source.getNameForMangaInfo() }, - isStubSource = remember { state.source is SourceManager.StubSource }, - favorite = state.manga.favorite, - status = state.manga.status, - trackingCount = state.trackingCount, - chapterCount = chapters.size, - chapterFiltered = state.manga.chaptersFiltered(), - incognitoMode = state.isIncognitoMode, - downloadedOnlyMode = state.isDownloadedOnlyMode, - fromSource = state.isFromSource, - onBackClicked = internalOnBackPressed, - onCoverClick = onCoverClicked, - onTagClicked = onTagClicked, - onAddToLibraryClicked = onAddToLibraryClicked, - onWebViewClicked = onWebViewClicked, - onTrackingClicked = onTrackingClicked, - onFilterButtonClicked = onFilterButtonClicked, - onShareClicked = onShareClicked, - onDownloadClicked = onDownloadActionClicked, - onEditCategoryClicked = onEditCategoryClicked, - onMigrateClicked = onMigrateClicked, - onEditInfoClicked = onEditInfoClicked, - onRecommendClicked = onRecommendClicked, - showRecommendsInOverflow = state.showRecommendationsInOverflow, - showMergedSettings = state.manga.source == MERGED_SOURCE_ID, - onMergedSettingsClicked = onMergedSettingsClicked, - onMergeClicked = onMergeClicked, - doGlobalSearch = onSearch, - showMergeWithAnother = state.showMergeWithAnother, - onMergeWithAnotherClicked = onMergeWithAnotherClicked, - mangaMetadataHeader = getDescriptionComposable( - source = remember { state.source.getMainSource>() }, - state = state, - openMetadataViewer = onMetadataViewerClicked, - search = { onSearch(it, false) }, - ), - searchMetadataChips = remember(state) { SearchMetadataChips(state.meta, state.source, state.manga.genre) }, - scrollBehavior = scrollBehavior, - actionModeCounter = selected.size, - onSelectAll = { - selected.clear() - selected.addAll(chapters) - }, - onInvertSelection = { - val toSelect = chapters - selected - selected.clear() - selected.addAll(toSelect) - }, - onSmallAppBarHeightChanged = onTopBarHeightChanged, + .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()), + ) + } + }, + ) { contentPadding -> + val noTopContentPadding = PaddingValues( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding(), + ) + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + val topPadding = contentPadding.calculateTopPadding() + + SwipeRefresh( + state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter), + onRefresh = onRefresh, + indicatorPadding = contentPadding, + indicator = { s, trigger -> + SwipeRefreshIndicator( + state = s, + refreshTriggerDistance = trigger, ) }, - bottomBar = { - SharedMangaBottomActionMenu( - selected = selected, - onMultiBookmarkClicked = onMultiBookmarkClicked, - onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, - onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, - onDownloadChapter = onDownloadChapter, - onMultiDeleteClicked = onMultiDeleteClicked, - fillFraction = 1f, - ) - }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - floatingActionButton = { - AnimatedVisibility( - visible = chapters.any { !it.chapter.read } && selected.isEmpty(), - enter = fadeIn(), - exit = fadeOut(), - ) { - ExtendedFloatingActionButton( - text = { - val id = if (chapters.any { it.chapter.read }) { - R.string.action_resume - } else { - R.string.action_start - } - Text(text = stringResource(id)) - }, - icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) }, - onClick = onContinueReading, - expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), - modifier = Modifier - .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()), - ) - } - }, - ) { contentPadding -> - val withNavBarContentPadding = contentPadding + - WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + ) { VerticalFastScroller( listState = chapterListState, - thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit }, - topContentPadding = withNavBarContentPadding.calculateTopPadding(), - endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current), + topContentPadding = topPadding, + endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection), ) { LazyColumn( modifier = Modifier.fillMaxHeight(), state = chapterListState, - contentPadding = withNavBarContentPadding, + contentPadding = noTopContentPadding, ) { + item(contentType = "info_box") { + MangaInfoBox( + windowWidthSizeClass = WindowWidthSizeClass.Compact, + appBarPadding = topPadding, + title = state.manga.title, + author = state.manga.author, + artist = state.manga.artist, + sourceName = remember { state.source.getNameForMangaInfo() }, + isStubSource = remember { state.source is SourceManager.StubSource }, + coverDataProvider = { state.manga }, + status = state.manga.status, + onCoverClick = onCoverClicked, + doSearch = onSearch, + ) + } + + item(contentType = "action_row") { + MangaActionRow( + favorite = state.manga.favorite, + trackingCount = state.trackingCount, + onAddToLibraryClicked = onAddToLibraryClicked, + onWebViewClicked = onWebViewClicked, + onTrackingClicked = onTrackingClicked, + onEditCategory = onEditCategoryClicked, + // SY --> + onMergeClicked = onMergeClicked, + // SY <-- + ) + } + + // SY --> + if (metadataSource != null) { + item(contentType = "metadata_info") { + metadataSource.DescriptionComposable( + state = state, + openMetadataViewer = onMetadataViewerClicked, + search = { onSearch(it, false) }, + ) + } + } + // SY <-- + + item(contentType = "desc") { + ExpandableMangaDescription( + defaultExpandState = state.isFromSource, + description = state.manga.description, + tagsProvider = { state.manga.genre }, + onTagClicked = onTagClicked, + // SY --> + doSearch = onSearch, + searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) { + SearchMetadataChips(state.meta, state.source, state.manga.genre) + }, + // SY <-- + ) + } + + // SY --> + if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) { + item(contentType = "info_buttons") { + MangaInfoButtons( + showRecommendsButton = !state.showRecommendationsInOverflow, + showMergeWithAnotherButton = state.showMergeWithAnother, + onRecommendClicked = onRecommendClicked, + onMergeWithAnotherClicked = onMergeWithAnotherClicked, + ) + } + } + // SY <-- + + item(contentType = "header") { + ChapterHeader( + chapterCount = chapters.size, + isChapterFiltered = state.manga.chaptersFiltered(), + onFilterButtonClicked = onFilterButtonClicked, + ) + } + sharedChapterItems( chapters = chapters, state = state, @@ -462,6 +494,10 @@ fun MangaScreenLargeImpl( val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current + // SY --> + val metadataSource = remember(state.source.id) { state.source.getMainSource>() } + // SY <-- + val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(0) } SwipeRefresh( @@ -571,45 +607,66 @@ fun MangaScreenLargeImpl( Row { val withNavBarContentPadding = contentPadding + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - MangaInfoHeader( + Column( modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) .padding(bottom = withNavBarContentPadding.calculateBottomPadding()), - windowWidthSizeClass = WindowWidthSizeClass.Expanded, - appBarPadding = contentPadding.calculateTopPadding(), - title = state.manga.title, - author = state.manga.author, - artist = state.manga.artist, - description = state.manga.description, - tagsProvider = { state.manga.genre }, - sourceName = remember { state.source.getNameForMangaInfo() }, - isStubSource = remember { state.source is SourceManager.StubSource }, - coverDataProvider = { state.manga }, - favorite = state.manga.favorite, - status = state.manga.status, - trackingCount = state.trackingCount, - fromSource = state.isFromSource, - onAddToLibraryClicked = onAddToLibraryClicked, - onWebViewClicked = onWebViewClicked, - onTrackingClicked = onTrackingClicked, - onMergeClicked = onMergeClicked, - onTagClicked = onTagClicked, - onEditCategory = onEditCategoryClicked, - onCoverClick = onCoverClicked, - doSearch = onSearch, - showRecommendsInOverflow = state.showRecommendationsInOverflow, - showMergeWithAnother = state.showMergeWithAnother, - onRecommendClicked = onRecommendClicked, - onMergeWithAnotherClicked = onMergeWithAnotherClicked, - mangaMetadataHeader = getDescriptionComposable( - source = remember { state.source.getMainSource>() }, + ) { + MangaInfoBox( + windowWidthSizeClass = windowWidthSizeClass, + appBarPadding = contentPadding.calculateTopPadding(), + title = state.manga.title, + author = state.manga.author, + artist = state.manga.artist, + sourceName = remember { state.source.getNameForMangaInfo() }, + isStubSource = remember { state.source is SourceManager.StubSource }, + coverDataProvider = { state.manga }, + status = state.manga.status, + onCoverClick = onCoverClicked, + doSearch = onSearch, + ) + MangaActionRow( + favorite = state.manga.favorite, + trackingCount = state.trackingCount, + onAddToLibraryClicked = onAddToLibraryClicked, + onWebViewClicked = onWebViewClicked, + onTrackingClicked = onTrackingClicked, + onEditCategory = onEditCategoryClicked, + // SY --> + onMergeClicked = onMergeClicked, + // SY <-- + ) + // SY --> + metadataSource?.DescriptionComposable( state = state, openMetadataViewer = onMetadataViewerClicked, search = { onSearch(it, false) }, - ), - searchMetadataChips = remember(state) { SearchMetadataChips(state.meta, state.source, state.manga.genre) }, - ) + ) + // SY <-- + ExpandableMangaDescription( + defaultExpandState = true, + description = state.manga.description, + tagsProvider = { state.manga.genre }, + onTagClicked = onTagClicked, + // SY --> + doSearch = onSearch, + searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) { + SearchMetadataChips(state.meta, state.source, state.manga.genre) + }, + // SY <-- + ) + // SY --> + if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) { + MangaInfoButtons( + showRecommendsButton = !state.showRecommendationsInOverflow, + showMergeWithAnotherButton = state.showMergeWithAnother, + onRecommendClicked = onRecommendClicked, + onMergeWithAnotherClicked = onMergeWithAnotherClicked, + ) + } + // SY <-- + } val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f VerticalFastScroller( @@ -727,7 +784,7 @@ private fun LazyListScope.sharedChapterItems( } } val lastPageRead = remember(chapter.lastPageRead) { - chapter.lastPageRead.takeIf { !chapter.read && it > 0 } + chapter.lastPageRead.takeIf { /* SY --> */(!chapter.read || state.alwaysShowPageProgress)/* SY <-- */ && it > 0 } } val scanlator = remember(chapter.scanlator) { chapter.scanlator.takeIf { !it.isNullOrBlank() } } @@ -840,19 +897,3 @@ private fun onChapterItemClick( else -> onChapterClicked(chapterItem.chapter) } } - -@Composable -@ReadOnlyComposable -@Stable -private fun getDescriptionComposable( - source: MetadataSource<*, *>?, - state: MangaScreenState.Success, - openMetadataViewer: () -> Unit, - search: (String) -> Unit, -): (@Composable () -> Unit)? { - return if (source != null) { - @Composable { - source.DescriptionComposable(state = state, openMetadataViewer = openMetadataViewer, search = search) - } - } else null -} 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 b977e5524..edb542fe5 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 @@ -83,219 +83,221 @@ import kotlin.math.roundToInt private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) @Composable -fun MangaInfoHeader( +fun MangaInfoBox( modifier: Modifier = Modifier, windowWidthSizeClass: WindowWidthSizeClass, appBarPadding: Dp, title: String, author: String?, artist: String?, - description: String?, - tagsProvider: () -> List?, sourceName: String, isStubSource: Boolean, coverDataProvider: () -> Manga, - favorite: Boolean, status: Long, + onCoverClick: () -> Unit, + doSearch: (query: String, global: Boolean) -> Unit, +) { + Box(modifier = modifier) { + // Backdrop + val backdropGradientColors = listOf( + Color.Transparent, + MaterialTheme.colorScheme.background, + ) + AsyncImage( + model = coverDataProvider(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .drawWithContent { + drawContent() + drawRect( + brush = Brush.verticalGradient(colors = backdropGradientColors), + ) + } + .alpha(.2f), + ) + + // Manga & source info + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { + if (windowWidthSizeClass == WindowWidthSizeClass.Compact) { + MangaAndSourceTitlesSmall( + appBarPadding = appBarPadding, + coverDataProvider = coverDataProvider, + onCoverClick = onCoverClick, + title = title, + context = LocalContext.current, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, + ) + } else { + MangaAndSourceTitlesLarge( + appBarPadding = appBarPadding, + coverDataProvider = coverDataProvider, + onCoverClick = onCoverClick, + title = title, + context = LocalContext.current, + doSearch = doSearch, + author = author, + artist = artist, + status = status, + sourceName = sourceName, + isStubSource = isStubSource, + ) + } + } + } +} + +@Composable +fun MangaActionRow( + modifier: Modifier = Modifier, + favorite: Boolean, trackingCount: Int, - fromSource: Boolean, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?, - onMergeClicked: () -> Unit, - onTagClicked: (String) -> Unit, onEditCategory: (() -> Unit)?, - onCoverClick: () -> Unit, - doSearch: (query: String, global: Boolean) -> Unit, - onRecommendClicked: () -> Unit, - showRecommendsInOverflow: Boolean, - showMergeWithAnother: Boolean, - onMergeWithAnotherClicked: () -> Unit, - mangaMetadataHeader: (@Composable () -> Unit)?, + // SY --> + onMergeClicked: () -> Unit, + // SY <-- +) { + Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { + val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) + MangaActionButton( + title = if (favorite) { + stringResource(R.string.in_library) + } else { + stringResource(R.string.add_to_library) + }, + icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, + color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor, + onClick = onAddToLibraryClicked, + onLongClick = onEditCategory, + ) + if (onTrackingClicked != null) { + MangaActionButton( + title = if (trackingCount == 0) { + stringResource(R.string.manga_tracking_tab) + } else { + quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount) + }, + icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done, + color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, + onClick = onTrackingClicked, + ) + } + if (onWebViewClicked != null) { + MangaActionButton( + title = stringResource(R.string.action_web_view), + icon = Icons.Default.Public, + color = defaultActionButtonColor, + onClick = onWebViewClicked, + ) + } + // SY --> + MangaActionButton( + title = stringResource(R.string.merge), + icon = Icons.Outlined.CallMerge, + color = defaultActionButtonColor, + onClick = onMergeClicked, + ) + // SY <-- + } +} + +@Composable +fun ExpandableMangaDescription( + modifier: Modifier = Modifier, + defaultExpandState: Boolean, + description: String?, + tagsProvider: () -> List?, + onTagClicked: (String) -> Unit, + // SY --> searchMetadataChips: SearchMetadataChips?, + doSearch: (query: String, global: Boolean) -> Unit, + // SY <-- ) { val context = LocalContext.current Column(modifier = modifier) { - Box { - // Backdrop - val backdropGradientColors = listOf( - Color.Transparent, - MaterialTheme.colorScheme.background, - ) - AsyncImage( - model = coverDataProvider(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .matchParentSize() - .drawWithContent { - drawContent() - drawRect( - brush = Brush.verticalGradient(colors = backdropGradientColors), - ) - } - .alpha(.2f), - ) - - // Manga & source info - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { - if (windowWidthSizeClass == WindowWidthSizeClass.Compact) { - MangaAndSourceTitlesSmall( - appBarPadding = appBarPadding, - coverDataProvider = coverDataProvider, - onCoverClick = onCoverClick, - title = title, - context = context, - doSearch = doSearch, - author = author, - artist = artist, - status = status, - sourceName = sourceName, - isStubSource = isStubSource, - ) - } else { - MangaAndSourceTitlesLarge( - appBarPadding = appBarPadding, - coverDataProvider = coverDataProvider, - onCoverClick = onCoverClick, - title = title, - context = context, - doSearch = doSearch, - author = author, - artist = artist, - status = status, - sourceName = sourceName, - isStubSource = isStubSource, - ) - } - } + val (expanded, onExpanded) = rememberSaveable { + mutableStateOf(defaultExpandState) } - - // Action buttons - Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { - val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) - MangaActionButton( - title = if (favorite) { - stringResource(R.string.in_library) - } else { - stringResource(R.string.add_to_library) - }, - icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, - color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor, - onClick = onAddToLibraryClicked, - onLongClick = onEditCategory, - ) - if (onTrackingClicked != null) { - MangaActionButton( - title = if (trackingCount == 0) { - stringResource(R.string.manga_tracking_tab) - } else { - quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount) - }, - icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done, - color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, - onClick = onTrackingClicked, - ) - } - if (onWebViewClicked != null) { - MangaActionButton( - title = stringResource(R.string.action_web_view), - icon = Icons.Default.Public, - color = defaultActionButtonColor, - onClick = onWebViewClicked, - ) - } - MangaActionButton( - title = stringResource(R.string.merge), - icon = Icons.Outlined.CallMerge, - color = defaultActionButtonColor, - onClick = onMergeClicked, - ) + val desc = + description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) + val trimmedDescription = remember(desc) { + desc + .replace(whitespaceLineRegex, "\n") + .trimEnd() } - - // SY --> Manga metadata - mangaMetadataHeader?.invoke() - // SY <-- - - // Expandable description-tags - Column { - val (expanded, onExpanded) = rememberSaveable { - mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact) - } - val desc = - description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) - val trimmedDescription = remember(desc) { - desc - .replace(whitespaceLineRegex, "\n") - .trimEnd() - } - MangaSummary( - expandedDescription = desc, - shrunkDescription = trimmedDescription, - expanded = expanded, + MangaSummary( + expandedDescription = desc, + shrunkDescription = trimmedDescription, + expanded = expanded, + modifier = Modifier + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + .clickableNoIndication( + onLongClick = { context.copyToClipboard(desc, desc) }, + onClick = { onExpanded(!expanded) }, + ), + ) + val tags = tagsProvider() + if (!tags.isNullOrEmpty()) { + Box( modifier = Modifier .padding(top = 8.dp) - .padding(horizontal = 16.dp) - .clickableNoIndication( - onLongClick = { context.copyToClipboard(desc, desc) }, - onClick = { onExpanded(!expanded) }, - ), - ) - val tags = tagsProvider() - if (!tags.isNullOrEmpty()) { - Box( - modifier = Modifier - .padding(top = 8.dp) - .padding(vertical = 12.dp) - .animateContentSize(), - ) { - if (expanded) { - if (searchMetadataChips != null) { - NamespaceTags( - tags = searchMetadataChips, - onClick = onTagClicked, - onLongClick = { doSearch(it, true) }, - ) - } else { - FlowRow( - modifier = Modifier.padding(horizontal = 16.dp), - mainAxisSpacing = 4.dp, - crossAxisSpacing = 8.dp, - ) { - tags.forEach { - TagsChip( - text = it, - onClick = { onTagClicked(it) }, - onLongClick = { doSearch(it, true) }, - ) - } - } - } + .padding(vertical = 12.dp) + .animateContentSize(), + ) { + if (expanded) { + // SY --> + if (searchMetadataChips != null) { + NamespaceTags( + tags = searchMetadataChips, + onClick = onTagClicked, + onLongClick = { doSearch(it, true) }, + ) } else { - LazyRow( - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), + // SY <-- + FlowRow( + modifier = Modifier.padding(horizontal = 16.dp), + mainAxisSpacing = 4.dp, + crossAxisSpacing = 8.dp, ) { - items(items = tags) { + tags.forEach { TagsChip( text = it, onClick = { onTagClicked(it) }, + // SY --> onLongClick = { doSearch(it, true) }, + // SY <-- ) } } } + } else { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + items(items = tags) { + TagsChip( + text = it, + onClick = { onTagClicked(it) }, + // SY --> + onLongClick = { doSearch(it, true) }, + // SY <-- + ) + } + } } } } - // SY --> - MangaInfoButtons( - showRecommendsButton = !showRecommendsInOverflow, - showMergeWithAnotherButton = showMergeWithAnother, - onRecommendClicked = onRecommendClicked, - onMergeWithAnotherClicked = onMergeWithAnotherClicked, - ) - // SY <-- } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt index 78c7e18ff..fe2531d9c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaSmallAppBar.kt @@ -54,12 +54,14 @@ fun MangaSmallAppBar( onDownloadClicked: ((DownloadAction) -> Unit)?, onEditCategoryClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?, + // SY --> showEditInfo: Boolean, onEditInfoClicked: () -> Unit, showRecommends: Boolean, onRecommendClicked: () -> Unit, showMergeSettings: Boolean, onMergedSettingsClicked: () -> Unit, + // SY <-- // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, 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 e2e304389..000000000 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt +++ /dev/null @@ -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?, - 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 - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index cb29b1f83..e5cc0a775 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -306,6 +306,7 @@ class MangaPresenter( mergedData = mergedData, showRecommendationsInOverflow = preferences.recommendsInOverflow().get(), showMergeWithAnother = smartSearched, + alwaysShowPageProgress = preferences.preserveReadingPosition().get() && manga.isEhBasedManga(), ) } @@ -1333,6 +1334,7 @@ sealed class MangaScreenState { val mergedData: MergedMangaData?, val showRecommendationsInOverflow: Boolean, val showMergeWithAnother: Boolean, + val alwaysShowPageProgress: Boolean, // SY <-- ) : MangaScreenState() {