Prechádzať zdrojové kódy

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 rokov pred
rodič
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.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.rememberScrollableState
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.WindowInsets
@@ -36,10 +32,10 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
-import androidx.compose.material3.rememberTopAppBarScrollState
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshots.SnapshotStateList
@@ -47,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
@@ -63,12 +58,12 @@ import eu.kanade.presentation.components.Scaffold
 import eu.kanade.presentation.components.SwipeRefreshIndicator
 import eu.kanade.presentation.components.VerticalFastScroller
 import eu.kanade.presentation.manga.components.ChapterHeader
+import eu.kanade.presentation.manga.components.ExpandableMangaDescription
+import eu.kanade.presentation.manga.components.MangaActionRow
 import eu.kanade.presentation.manga.components.MangaBottomActionMenu
 import eu.kanade.presentation.manga.components.MangaChapterListItem
-import eu.kanade.presentation.manga.components.MangaInfoHeader
+import eu.kanade.presentation.manga.components.MangaInfoBox
 import eu.kanade.presentation.manga.components.MangaSmallAppBar
-import eu.kanade.presentation.manga.components.MangaTopAppBar
-import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior
 import eu.kanade.presentation.util.isScrolledToEnd
 import eu.kanade.presentation.util.isScrollingUp
 import eu.kanade.presentation.util.plus
@@ -79,7 +74,6 @@ import eu.kanade.tachiyomi.source.getNameForMangaInfo
 import eu.kanade.tachiyomi.ui.manga.ChapterItem
 import eu.kanade.tachiyomi.ui.manga.MangaScreenState
 import eu.kanade.tachiyomi.util.lang.toRelativeString
-import kotlinx.coroutines.runBlocking
 import java.text.DecimalFormat
 import java.text.DecimalFormatSymbols
 import java.util.Date
@@ -208,160 +202,169 @@ private fun MangaScreenSmallImpl(
     onMultiDeleteClicked: (List<Chapter>) -> Unit,
 ) {
     val layoutDirection = LocalLayoutDirection.current
-    val decayAnimationSpec = rememberSplineBasedDecay<Float>()
-    val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec)
     val chapterListState = rememberLazyListState()
-    SideEffect {
-        if (chapterListState.firstVisibleItemIndex > 0 || chapterListState.firstVisibleItemScrollOffset > 0) {
-            // Should go here after a configuration change
-            // Safe to say that the app bar is fully scrolled
-            scrollBehavior.state.offset = scrollBehavior.state.offsetLimit
-        }
-    }
 
     val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
-    val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) }
-    SwipeRefresh(
-        state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter),
-        onRefresh = onRefresh,
-        indicatorPadding = PaddingValues(
-            start = insetPadding.calculateStartPadding(layoutDirection),
-            top = with(LocalDensity.current) { topBarHeight.toDp() },
-            end = insetPadding.calculateEndPadding(layoutDirection),
-        ),
-        indicator = { s, trigger ->
-            SwipeRefreshIndicator(
-                state = s,
-                refreshTriggerDistance = trigger,
-            )
-        },
-    ) {
-        val chapters = remember(state) { state.processedChapters.toList() }
-        val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() }
-        val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list
+    val 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(
                 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(

+ 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))
 
 @Composable
-fun MangaInfoHeader(
+fun MangaInfoBox(
     modifier: Modifier = Modifier,
     windowWidthSizeClass: WindowWidthSizeClass,
     appBarPadding: Dp,
     title: String,
     author: String?,
     artist: String?,
-    description: String?,
-    tagsProvider: () -> List<String>?,
     sourceName: String,
     isStubSource: Boolean,
     coverDataProvider: () -> Manga,
-    favorite: Boolean,
     status: Long,
-    trackingCount: Int,
-    fromSource: Boolean,
-    onAddToLibraryClicked: () -> Unit,
-    onWebViewClicked: (() -> Unit)?,
-    onTrackingClicked: (() -> Unit)?,
-    onTagClicked: (String) -> Unit,
-    onEditCategory: (() -> Unit)?,
     onCoverClick: () -> Unit,
     doSearch: (query: String, global: Boolean) -> Unit,
 ) {
-    val context = LocalContext.current
-    Column(modifier = modifier) {
-        Box {
-            // Backdrop
-            val backdropGradientColors = listOf(
-                Color.Transparent,
-                MaterialTheme.colorScheme.background,
-            )
-            AsyncImage(
-                model = coverDataProvider(),
-                contentDescription = null,
-                contentScale = ContentScale.Crop,
-                modifier = Modifier
-                    .matchParentSize()
-                    .drawWithContent {
-                        drawContent()
-                        drawRect(
-                            brush = Brush.verticalGradient(colors = backdropGradientColors),
-                        )
-                    }
-                    .alpha(.2f),
-            )
-
-            // Manga & source info
-            CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
-                if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
-                    MangaAndSourceTitlesSmall(
-                        appBarPadding = appBarPadding,
-                        coverDataProvider = coverDataProvider,
-                        onCoverClick = onCoverClick,
-                        title = title,
-                        context = context,
-                        doSearch = doSearch,
-                        author = author,
-                        artist = artist,
-                        status = status,
-                        sourceName = sourceName,
-                        isStubSource = isStubSource,
-                    )
-                } else {
-                    MangaAndSourceTitlesLarge(
-                        appBarPadding = appBarPadding,
-                        coverDataProvider = coverDataProvider,
-                        onCoverClick = onCoverClick,
-                        title = title,
-                        context = context,
-                        doSearch = doSearch,
-                        author = author,
-                        artist = artist,
-                        status = status,
-                        sourceName = sourceName,
-                        isStubSource = isStubSource,
+    Box(modifier = modifier) {
+        // Backdrop
+        val backdropGradientColors = listOf(
+            Color.Transparent,
+            MaterialTheme.colorScheme.background,
+        )
+        AsyncImage(
+            model = coverDataProvider(),
+            contentDescription = null,
+            contentScale = ContentScale.Crop,
+            modifier = Modifier
+                .matchParentSize()
+                .drawWithContent {
+                    drawContent()
+                    drawRect(
+                        brush = Brush.verticalGradient(colors = backdropGradientColors),
                     )
                 }
+                .alpha(.2f),
+        )
+
+        // Manga & source info
+        CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
+            if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
+                MangaAndSourceTitlesSmall(
+                    appBarPadding = appBarPadding,
+                    coverDataProvider = coverDataProvider,
+                    onCoverClick = onCoverClick,
+                    title = title,
+                    context = LocalContext.current,
+                    doSearch = doSearch,
+                    author = author,
+                    artist = artist,
+                    status = status,
+                    sourceName = sourceName,
+                    isStubSource = isStubSource,
+                )
+            } else {
+                MangaAndSourceTitlesLarge(
+                    appBarPadding = appBarPadding,
+                    coverDataProvider = coverDataProvider,
+                    onCoverClick = onCoverClick,
+                    title = title,
+                    context = LocalContext.current,
+                    doSearch = doSearch,
+                    author = author,
+                    artist = artist,
+                    status = status,
+                    sourceName = sourceName,
+                    isStubSource = isStubSource,
+                )
             }
         }
+    }
+}
 
-        // Action buttons
-        Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
-            val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
+@Composable
+fun MangaActionRow(
+    modifier: Modifier = Modifier,
+    favorite: Boolean,
+    trackingCount: Int,
+    onAddToLibraryClicked: () -> Unit,
+    onWebViewClicked: (() -> Unit)?,
+    onTrackingClicked: (() -> Unit)?,
+    onEditCategory: (() -> Unit)?,
+) {
+    Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
+        val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
+        MangaActionButton(
+            title = if (favorite) {
+                stringResource(R.string.in_library)
+            } else {
+                stringResource(R.string.add_to_library)
+            },
+            icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
+            color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
+            onClick = onAddToLibraryClicked,
+            onLongClick = onEditCategory,
+        )
+        if (onTrackingClicked != null) {
             MangaActionButton(
-                title = if (favorite) {
-                    stringResource(R.string.in_library)
+                title = if (trackingCount == 0) {
+                    stringResource(R.string.manga_tracking_tab)
                 } else {
-                    stringResource(R.string.add_to_library)
+                    quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
                 },
-                icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
-                color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
-                onClick = onAddToLibraryClicked,
-                onLongClick = onEditCategory,
+                icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
+                color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
+                onClick = onTrackingClicked,
             )
-            if (onTrackingClicked != null) {
-                MangaActionButton(
-                    title = if (trackingCount == 0) {
-                        stringResource(R.string.manga_tracking_tab)
-                    } else {
-                        quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
-                    },
-                    icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
-                    color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
-                    onClick = onTrackingClicked,
-                )
-            }
-            if (onWebViewClicked != null) {
-                MangaActionButton(
-                    title = stringResource(R.string.action_web_view),
-                    icon = Icons.Default.Public,
-                    color = defaultActionButtonColor,
-                    onClick = onWebViewClicked,
-                )
-            }
         }
+        if (onWebViewClicked != null) {
+            MangaActionButton(
+                title = stringResource(R.string.action_web_view),
+                icon = Icons.Default.Public,
+                color = defaultActionButtonColor,
+                onClick = onWebViewClicked,
+            )
+        }
+    }
+}
 
-        // Expandable description-tags
-        Column {
-            val (expanded, onExpanded) = rememberSaveable {
-                mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
-            }
-            val desc =
-                description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
-            val trimmedDescription = remember(desc) {
-                desc
-                    .replace(whitespaceLineRegex, "\n")
-                    .trimEnd()
-            }
-            MangaSummary(
-                expandedDescription = desc,
-                shrunkDescription = trimmedDescription,
-                expanded = expanded,
+@Composable
+fun ExpandableMangaDescription(
+    modifier: Modifier = Modifier,
+    defaultExpandState: Boolean,
+    description: String?,
+    tagsProvider: () -> List<String>?,
+    onTagClicked: (String) -> Unit,
+) {
+    val context = LocalContext.current
+    Column(modifier = modifier) {
+        val (expanded, onExpanded) = rememberSaveable {
+            mutableStateOf(defaultExpandState)
+        }
+        val desc =
+            description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
+        val trimmedDescription = remember(desc) {
+            desc
+                .replace(whitespaceLineRegex, "\n")
+                .trimEnd()
+        }
+        MangaSummary(
+            expandedDescription = desc,
+            shrunkDescription = trimmedDescription,
+            expanded = expanded,
+            modifier = Modifier
+                .padding(top = 8.dp)
+                .padding(horizontal = 16.dp)
+                .clickableNoIndication(
+                    onLongClick = { context.copyToClipboard(desc, desc) },
+                    onClick = { onExpanded(!expanded) },
+                ),
+        )
+        val tags = tagsProvider()
+        if (!tags.isNullOrEmpty()) {
+            Box(
                 modifier = Modifier
                     .padding(top = 8.dp)
-                    .padding(horizontal = 16.dp)
-                    .clickableNoIndication(
-                        onLongClick = { context.copyToClipboard(desc, desc) },
-                        onClick = { onExpanded(!expanded) },
-                    ),
-            )
-            val tags = tagsProvider()
-            if (!tags.isNullOrEmpty()) {
-                Box(
-                    modifier = Modifier
-                        .padding(top = 8.dp)
-                        .padding(vertical = 12.dp)
-                        .animateContentSize(),
-                ) {
-                    if (expanded) {
-                        FlowRow(
-                            modifier = Modifier.padding(horizontal = 16.dp),
-                            mainAxisSpacing = 4.dp,
-                            crossAxisSpacing = 8.dp,
-                        ) {
-                            tags.forEach {
-                                TagsChip(
-                                    text = it,
-                                    onClick = { onTagClicked(it) },
-                                )
-                            }
+                    .padding(vertical = 12.dp)
+                    .animateContentSize(),
+            ) {
+                if (expanded) {
+                    FlowRow(
+                        modifier = Modifier.padding(horizontal = 16.dp),
+                        mainAxisSpacing = 4.dp,
+                        crossAxisSpacing = 8.dp,
+                    ) {
+                        tags.forEach {
+                            TagsChip(
+                                text = it,
+                                onClick = { onTagClicked(it) },
+                            )
                         }
-                    } else {
-                        LazyRow(
-                            contentPadding = PaddingValues(horizontal = 16.dp),
-                            horizontalArrangement = Arrangement.spacedBy(4.dp),
-                        ) {
-                            items(items = tags) {
-                                TagsChip(
-                                    text = it,
-                                    onClick = { onTagClicked(it) },
-                                )
-                            }
+                    }
+                } else {
+                    LazyRow(
+                        contentPadding = PaddingValues(horizontal = 16.dp),
+                        horizontalArrangement = Arrangement.spacedBy(4.dp),
+                    ) {
+                        items(items = tags) {
+                            TagsChip(
+                                text = it,
+                                onClick = { onTagClicked(it) },
+                            )
                         }
                     }
                 }

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