Browse Source

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.
Ivan Iskandar 2 years ago
parent
commit
34906a7425

+ 183 - 173
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.activity.compose.BackHandler
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.fadeOut
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.rememberScrollableState
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsets
@@ -36,10 +32,10 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
-import androidx.compose.material3.rememberTopAppBarScrollState
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshots.SnapshotStateList
 import androidx.compose.runtime.snapshots.SnapshotStateList
@@ -47,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalDensity
@@ -63,12 +58,12 @@ import eu.kanade.presentation.components.Scaffold
 import eu.kanade.presentation.components.SwipeRefreshIndicator
 import eu.kanade.presentation.components.SwipeRefreshIndicator
 import eu.kanade.presentation.components.VerticalFastScroller
 import eu.kanade.presentation.components.VerticalFastScroller
 import eu.kanade.presentation.manga.components.ChapterHeader
 import eu.kanade.presentation.manga.components.ChapterHeader
+import eu.kanade.presentation.manga.components.ExpandableMangaDescription
+import eu.kanade.presentation.manga.components.MangaActionRow
 import eu.kanade.presentation.manga.components.MangaBottomActionMenu
 import eu.kanade.presentation.manga.components.MangaBottomActionMenu
 import eu.kanade.presentation.manga.components.MangaChapterListItem
 import eu.kanade.presentation.manga.components.MangaChapterListItem
-import eu.kanade.presentation.manga.components.MangaInfoHeader
+import eu.kanade.presentation.manga.components.MangaInfoBox
 import eu.kanade.presentation.manga.components.MangaSmallAppBar
 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.isScrolledToEnd
 import eu.kanade.presentation.util.isScrollingUp
 import eu.kanade.presentation.util.isScrollingUp
 import eu.kanade.presentation.util.plus
 import eu.kanade.presentation.util.plus
@@ -79,7 +74,6 @@ import eu.kanade.tachiyomi.source.getNameForMangaInfo
 import eu.kanade.tachiyomi.ui.manga.ChapterItem
 import eu.kanade.tachiyomi.ui.manga.ChapterItem
 import eu.kanade.tachiyomi.ui.manga.MangaScreenState
 import eu.kanade.tachiyomi.ui.manga.MangaScreenState
 import eu.kanade.tachiyomi.util.lang.toRelativeString
 import eu.kanade.tachiyomi.util.lang.toRelativeString
-import kotlinx.coroutines.runBlocking
 import java.text.DecimalFormat
 import java.text.DecimalFormat
 import java.text.DecimalFormatSymbols
 import java.text.DecimalFormatSymbols
 import java.util.Date
 import java.util.Date
@@ -208,160 +202,169 @@ private fun MangaScreenSmallImpl(
     onMultiDeleteClicked: (List<Chapter>) -> Unit,
     onMultiDeleteClicked: (List<Chapter>) -> Unit,
 ) {
 ) {
     val layoutDirection = LocalLayoutDirection.current
     val layoutDirection = LocalLayoutDirection.current
-    val decayAnimationSpec = rememberSplineBasedDecay<Float>()
-    val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec)
     val chapterListState = rememberLazyListState()
     val chapterListState = rememberLazyListState()
-    SideEffect {
-        if (chapterListState.firstVisibleItemIndex > 0 || chapterListState.firstVisibleItemScrollOffset > 0) {
-            // Should go here after a configuration change
-            // Safe to say that the app bar is fully scrolled
-            scrollBehavior.state.offset = scrollBehavior.state.offsetLimit
-        }
-    }
 
 
     val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
     val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
-    val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) }
-    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 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()
-            }
+    val internalOnBackPressed = {
+        if (selected.isNotEmpty()) {
+            selected.clear()
+        } else {
+            onBackClicked()
         }
         }
-        BackHandler(onBack = internalOnBackPressed)
+    }
+    BackHandler(onBack = internalOnBackPressed)
 
 
-        Scaffold(
-            modifier = Modifier
-                .nestedScroll(scrollBehavior.nestedScrollConnection)
-                .padding(insetPadding),
-            topBar = {
-                MangaTopAppBar(
-                    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)
+    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)
+                },
+            )
+        },
+        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))
                     },
                     },
-                    onSmallAppBarHeightChanged = onTopBarHeightChanged,
+                    icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
+                    onClick = onContinueReading,
+                    expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
+                    modifier = Modifier
+                        .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
                 )
                 )
-            },
-            bottomBar = {
-                SharedMangaBottomActionMenu(
-                    selected = selected,
-                    onMultiBookmarkClicked = onMultiBookmarkClicked,
-                    onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
-                    onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked,
-                    onDownloadChapter = onDownloadChapter,
-                    onMultiDeleteClicked = onMultiDeleteClicked,
-                    fillFraction = 1f,
+            }
+        },
+    ) { 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,
                 )
                 )
             },
             },
-            snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
-            floatingActionButton = {
-                AnimatedVisibility(
-                    visible = chapters.any { !it.chapter.read } && selected.isEmpty(),
-                    enter = fadeIn(),
-                    exit = fadeOut(),
-                ) {
-                    ExtendedFloatingActionButton(
-                        text = {
-                            val id = if (chapters.any { it.chapter.read }) {
-                                R.string.action_resume
-                            } else {
-                                R.string.action_start
-                            }
-                            Text(text = stringResource(id))
-                        },
-                        icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) },
-                        onClick = onContinueReading,
-                        expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(),
-                        modifier = Modifier
-                            .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()),
-                    )
-                }
-            },
-        ) { contentPadding ->
-            val withNavBarContentPadding = contentPadding +
-                WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
+        ) {
             VerticalFastScroller(
             VerticalFastScroller(
                 listState = chapterListState,
                 listState = chapterListState,
-                thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit },
-                topContentPadding = withNavBarContentPadding.calculateTopPadding(),
-                endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current),
+                topContentPadding = topPadding,
+                endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection),
             ) {
             ) {
                 LazyColumn(
                 LazyColumn(
                     modifier = Modifier.fillMaxHeight(),
                     modifier = Modifier.fillMaxHeight(),
                     state = chapterListState,
                     state = chapterListState,
-                    contentPadding = withNavBarContentPadding,
+                    contentPadding = noTopContentPadding,
                 ) {
                 ) {
+                    item(contentType = "info_box") {
+                        MangaInfoBox(
+                            windowWidthSizeClass = WindowWidthSizeClass.Compact,
+                            appBarPadding = topPadding,
+                            title = state.manga.title,
+                            author = state.manga.author,
+                            artist = state.manga.artist,
+                            sourceName = remember { state.source.getNameForMangaInfo() },
+                            isStubSource = remember { state.source is SourceManager.StubSource },
+                            coverDataProvider = { state.manga },
+                            status = state.manga.status,
+                            onCoverClick = onCoverClicked,
+                            doSearch = onSearch,
+                        )
+                    }
+
+                    item(contentType = "action_row") {
+                        MangaActionRow(
+                            favorite = state.manga.favorite,
+                            trackingCount = state.trackingCount,
+                            onAddToLibraryClicked = onAddToLibraryClicked,
+                            onWebViewClicked = onWebViewClicked,
+                            onTrackingClicked = onTrackingClicked,
+                            onEditCategory = onEditCategoryClicked,
+                        )
+                    }
+
+                    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(
                     sharedChapterItems(
                         chapters = chapters,
                         chapters = chapters,
                         state = state,
                         state = state,
@@ -514,33 +517,40 @@ fun MangaScreenLargeImpl(
             Row {
             Row {
                 val withNavBarContentPadding = contentPadding +
                 val withNavBarContentPadding = contentPadding +
                     WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
                     WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
-                MangaInfoHeader(
+                Column(
                     modifier = Modifier
                     modifier = Modifier
                         .weight(1f)
                         .weight(1f)
                         .verticalScroll(rememberScrollState())
                         .verticalScroll(rememberScrollState())
                         .padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
                         .padding(bottom = withNavBarContentPadding.calculateBottomPadding()),
-                    windowWidthSizeClass = WindowWidthSizeClass.Expanded,
-                    appBarPadding = contentPadding.calculateTopPadding(),
-                    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
                 val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f
                 VerticalFastScroller(
                 VerticalFastScroller(

+ 152 - 146
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))
 private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
 
 
 @Composable
 @Composable
-fun MangaInfoHeader(
+fun MangaInfoBox(
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     windowWidthSizeClass: WindowWidthSizeClass,
     windowWidthSizeClass: WindowWidthSizeClass,
     appBarPadding: Dp,
     appBarPadding: Dp,
     title: String,
     title: String,
     author: String?,
     author: String?,
     artist: String?,
     artist: String?,
-    description: String?,
-    tagsProvider: () -> List<String>?,
     sourceName: String,
     sourceName: String,
     isStubSource: Boolean,
     isStubSource: Boolean,
     coverDataProvider: () -> Manga,
     coverDataProvider: () -> Manga,
-    favorite: Boolean,
     status: Long,
     status: Long,
-    trackingCount: Int,
-    fromSource: Boolean,
-    onAddToLibraryClicked: () -> Unit,
-    onWebViewClicked: (() -> Unit)?,
-    onTrackingClicked: (() -> Unit)?,
-    onTagClicked: (String) -> Unit,
-    onEditCategory: (() -> Unit)?,
     onCoverClick: () -> Unit,
     onCoverClick: () -> Unit,
     doSearch: (query: String, global: Boolean) -> 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(
             MangaActionButton(
-                title = if (favorite) {
-                    stringResource(R.string.in_library)
+                title = if (trackingCount == 0) {
+                    stringResource(R.string.manga_tracking_tab)
                 } else {
                 } 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
                 modifier = Modifier
                     .padding(top = 8.dp)
                     .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) },
+                            )
                         }
                         }
                     }
                     }
                 }
                 }

+ 0 - 141
app/src/main/java/eu/kanade/presentation/manga/components/MangaTopAppBar.kt

@@ -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
-            }
-        }
-    }
-}