mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	MangaScreen: Ditch the expanded app bar (#7470)
Animating the content padding that's used for the lazy list is heavy. A simple fix to *just* offset the list is blocked by a Compose fling issue (b/179417109). So I decided to go with the previous layout of this screen by putting everything in the list. MangaInfoHeader is split into separate composables to avoid jank when the item is being inflated.
This commit is contained in:
		| @@ -2,15 +2,11 @@ package eu.kanade.presentation.manga | ||||
|  | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.compose.animation.AnimatedVisibility | ||||
| import androidx.compose.animation.core.animateFloatAsState | ||||
| import androidx.compose.animation.fadeIn | ||||
| import androidx.compose.animation.fadeOut | ||||
| import androidx.compose.animation.rememberSplineBasedDecay | ||||
| import androidx.compose.foundation.gestures.Orientation | ||||
| import androidx.compose.foundation.gestures.rememberScrollableState | ||||
| import androidx.compose.foundation.gestures.scrollBy | ||||
| import androidx.compose.foundation.gestures.scrollable | ||||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| @@ -36,10 +32,10 @@ import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.SnackbarHost | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.rememberTopAppBarScrollState | ||||
| import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.SideEffect | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.snapshots.SnapshotStateList | ||||
| @@ -47,7 +43,6 @@ import androidx.compose.runtime.toMutableStateList | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | ||||
| import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||
| import androidx.compose.ui.layout.onSizeChanged | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| @@ -63,12 +58,12 @@ import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.components.SwipeRefreshIndicator | ||||
| import eu.kanade.presentation.components.VerticalFastScroller | ||||
| import eu.kanade.presentation.manga.components.ChapterHeader | ||||
| import eu.kanade.presentation.manga.components.ExpandableMangaDescription | ||||
| import eu.kanade.presentation.manga.components.MangaActionRow | ||||
| import eu.kanade.presentation.manga.components.MangaBottomActionMenu | ||||
| import eu.kanade.presentation.manga.components.MangaChapterListItem | ||||
| import eu.kanade.presentation.manga.components.MangaInfoHeader | ||||
| import eu.kanade.presentation.manga.components.MangaInfoBox | ||||
| import eu.kanade.presentation.manga.components.MangaSmallAppBar | ||||
| import eu.kanade.presentation.manga.components.MangaTopAppBar | ||||
| import eu.kanade.presentation.util.ExitUntilCollapsedScrollBehavior | ||||
| import eu.kanade.presentation.util.isScrolledToEnd | ||||
| import eu.kanade.presentation.util.isScrollingUp | ||||
| import eu.kanade.presentation.util.plus | ||||
| @@ -79,7 +74,6 @@ import eu.kanade.tachiyomi.source.getNameForMangaInfo | ||||
| import eu.kanade.tachiyomi.ui.manga.ChapterItem | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaScreenState | ||||
| import eu.kanade.tachiyomi.util.lang.toRelativeString | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import java.text.DecimalFormat | ||||
| import java.text.DecimalFormatSymbols | ||||
| import java.util.Date | ||||
| @@ -208,160 +202,169 @@ private fun MangaScreenSmallImpl( | ||||
|     onMultiDeleteClicked: (List<Chapter>) -> Unit, | ||||
| ) { | ||||
|     val layoutDirection = LocalLayoutDirection.current | ||||
|     val decayAnimationSpec = rememberSplineBasedDecay<Float>() | ||||
|     val scrollBehavior = ExitUntilCollapsedScrollBehavior(rememberTopAppBarScrollState(), decayAnimationSpec) | ||||
|     val chapterListState = rememberLazyListState() | ||||
|     SideEffect { | ||||
|         if (chapterListState.firstVisibleItemIndex > 0 || chapterListState.firstVisibleItemScrollOffset > 0) { | ||||
|             // Should go here after a configuration change | ||||
|             // Safe to say that the app bar is fully scrolled | ||||
|             scrollBehavior.state.offset = scrollBehavior.state.offsetLimit | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() | ||||
|     val (topBarHeight, onTopBarHeightChanged) = remember { mutableStateOf(1) } | ||||
|     SwipeRefresh( | ||||
|         state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter), | ||||
|         onRefresh = onRefresh, | ||||
|         indicatorPadding = PaddingValues( | ||||
|             start = insetPadding.calculateStartPadding(layoutDirection), | ||||
|             top = with(LocalDensity.current) { topBarHeight.toDp() }, | ||||
|             end = insetPadding.calculateEndPadding(layoutDirection), | ||||
|         ), | ||||
|         indicator = { s, trigger -> | ||||
|             SwipeRefreshIndicator( | ||||
|                 state = s, | ||||
|                 refreshTriggerDistance = trigger, | ||||
|     val chapters = remember(state) { state.processedChapters.toList() } | ||||
|     val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() } | ||||
|     val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list | ||||
|  | ||||
|     val internalOnBackPressed = { | ||||
|         if (selected.isNotEmpty()) { | ||||
|             selected.clear() | ||||
|         } else { | ||||
|             onBackClicked() | ||||
|         } | ||||
|     } | ||||
|     BackHandler(onBack = internalOnBackPressed) | ||||
|  | ||||
|     Scaffold( | ||||
|         modifier = Modifier | ||||
|             .padding(insetPadding), | ||||
|         topBar = { | ||||
|             val firstVisibleItemIndex by remember { | ||||
|                 derivedStateOf { chapterListState.firstVisibleItemIndex } | ||||
|             } | ||||
|             val firstVisibleItemScrollOffset by remember { | ||||
|                 derivedStateOf { chapterListState.firstVisibleItemScrollOffset } | ||||
|             } | ||||
|             val animatedTitleAlpha by animateFloatAsState( | ||||
|                 if (firstVisibleItemIndex > 0) 1f else 0f, | ||||
|             ) | ||||
|             val animatedBgAlpha by animateFloatAsState( | ||||
|                 if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f, | ||||
|             ) | ||||
|             MangaSmallAppBar( | ||||
|                 title = state.manga.title, | ||||
|                 titleAlphaProvider = { animatedTitleAlpha }, | ||||
|                 backgroundAlphaProvider = { animatedBgAlpha }, | ||||
|                 incognitoMode = state.isIncognitoMode, | ||||
|                 downloadedOnlyMode = state.isDownloadedOnlyMode, | ||||
|                 onBackClicked = onBackClicked, | ||||
|                 onShareClicked = onShareClicked, | ||||
|                 onDownloadClicked = onDownloadActionClicked, | ||||
|                 onEditCategoryClicked = onEditCategoryClicked, | ||||
|                 onMigrateClicked = onMigrateClicked, | ||||
|                 actionModeCounter = selected.size, | ||||
|                 onSelectAll = { | ||||
|                     selected.clear() | ||||
|                     selected.addAll(chapters) | ||||
|                 }, | ||||
|                 onInvertSelection = { | ||||
|                     val toSelect = chapters - selected | ||||
|                     selected.clear() | ||||
|                     selected.addAll(toSelect) | ||||
|                 }, | ||||
|             ) | ||||
|         }, | ||||
|     ) { | ||||
|         val chapters = remember(state) { state.processedChapters.toList() } | ||||
|         val selected = remember(chapters) { emptyList<ChapterItem>().toMutableStateList() } | ||||
|         val selectedPositions = remember(chapters) { arrayOf(-1, -1) } // first and last selected index in list | ||||
|  | ||||
|         val internalOnBackPressed = { | ||||
|             if (selected.isNotEmpty()) { | ||||
|                 selected.clear() | ||||
|             } else { | ||||
|                 onBackClicked() | ||||
|             } | ||||
|         } | ||||
|         BackHandler(onBack = internalOnBackPressed) | ||||
|  | ||||
|         Scaffold( | ||||
|             modifier = Modifier | ||||
|                 .nestedScroll(scrollBehavior.nestedScrollConnection) | ||||
|                 .padding(insetPadding), | ||||
|             topBar = { | ||||
|                 MangaTopAppBar( | ||||
|         bottomBar = { | ||||
|             SharedMangaBottomActionMenu( | ||||
|                 selected = selected, | ||||
|                 onMultiBookmarkClicked = onMultiBookmarkClicked, | ||||
|                 onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, | ||||
|                 onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, | ||||
|                 onDownloadChapter = onDownloadChapter, | ||||
|                 onMultiDeleteClicked = onMultiDeleteClicked, | ||||
|                 fillFraction = 1f, | ||||
|             ) | ||||
|         }, | ||||
|         snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|         floatingActionButton = { | ||||
|             AnimatedVisibility( | ||||
|                 visible = chapters.any { !it.chapter.read } && selected.isEmpty(), | ||||
|                 enter = fadeIn(), | ||||
|                 exit = fadeOut(), | ||||
|             ) { | ||||
|                 ExtendedFloatingActionButton( | ||||
|                     text = { | ||||
|                         val id = if (chapters.any { it.chapter.read }) { | ||||
|                             R.string.action_resume | ||||
|                         } else { | ||||
|                             R.string.action_start | ||||
|                         } | ||||
|                         Text(text = stringResource(id)) | ||||
|                     }, | ||||
|                     icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) }, | ||||
|                     onClick = onContinueReading, | ||||
|                     expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), | ||||
|                     modifier = Modifier | ||||
|                         .scrollable( | ||||
|                             state = rememberScrollableState { | ||||
|                                 var consumed = runBlocking { chapterListState.scrollBy(-it) } * -1 | ||||
|                                 if (consumed == 0f) { | ||||
|                                     // Pass scroll to app bar if we're on the top of the list | ||||
|                                     val newOffset = | ||||
|                                         (scrollBehavior.state.offset + it).coerceIn(scrollBehavior.state.offsetLimit, 0f) | ||||
|                                     consumed = newOffset - scrollBehavior.state.offset | ||||
|                                     scrollBehavior.state.offset = newOffset | ||||
|                                 } | ||||
|                                 consumed | ||||
|                             }, | ||||
|                             orientation = Orientation.Vertical, | ||||
|                             interactionSource = chapterListState.interactionSource as MutableInteractionSource, | ||||
|                         ), | ||||
|                     title = state.manga.title, | ||||
|                     author = state.manga.author, | ||||
|                     artist = state.manga.artist, | ||||
|                     description = state.manga.description, | ||||
|                     tagsProvider = { state.manga.genre }, | ||||
|                     coverDataProvider = { state.manga }, | ||||
|                     sourceName = remember { state.source.getNameForMangaInfo() }, | ||||
|                     isStubSource = remember { state.source is SourceManager.StubSource }, | ||||
|                     favorite = state.manga.favorite, | ||||
|                     status = state.manga.status, | ||||
|                     trackingCount = state.trackingCount, | ||||
|                     chapterCount = chapters.size, | ||||
|                     chapterFiltered = state.manga.chaptersFiltered(), | ||||
|                     incognitoMode = state.isIncognitoMode, | ||||
|                     downloadedOnlyMode = state.isDownloadedOnlyMode, | ||||
|                     fromSource = state.isFromSource, | ||||
|                     onBackClicked = internalOnBackPressed, | ||||
|                     onCoverClick = onCoverClicked, | ||||
|                     onTagClicked = onTagClicked, | ||||
|                     onAddToLibraryClicked = onAddToLibraryClicked, | ||||
|                     onWebViewClicked = onWebViewClicked, | ||||
|                     onTrackingClicked = onTrackingClicked, | ||||
|                     onFilterButtonClicked = onFilterButtonClicked, | ||||
|                     onShareClicked = onShareClicked, | ||||
|                     onDownloadClicked = onDownloadActionClicked, | ||||
|                     onEditCategoryClicked = onEditCategoryClicked, | ||||
|                     onMigrateClicked = onMigrateClicked, | ||||
|                     doGlobalSearch = onSearch, | ||||
|                     scrollBehavior = scrollBehavior, | ||||
|                     actionModeCounter = selected.size, | ||||
|                     onSelectAll = { | ||||
|                         selected.clear() | ||||
|                         selected.addAll(chapters) | ||||
|                     }, | ||||
|                     onInvertSelection = { | ||||
|                         val toSelect = chapters - selected | ||||
|                         selected.clear() | ||||
|                         selected.addAll(toSelect) | ||||
|                     }, | ||||
|                     onSmallAppBarHeightChanged = onTopBarHeightChanged, | ||||
|                         .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()), | ||||
|                 ) | ||||
|             } | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         val noTopContentPadding = PaddingValues( | ||||
|             start = contentPadding.calculateStartPadding(layoutDirection), | ||||
|             end = contentPadding.calculateEndPadding(layoutDirection), | ||||
|             bottom = contentPadding.calculateBottomPadding(), | ||||
|         ) + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() | ||||
|         val topPadding = contentPadding.calculateTopPadding() | ||||
|  | ||||
|         SwipeRefresh( | ||||
|             state = rememberSwipeRefreshState(state.isRefreshingInfo || state.isRefreshingChapter), | ||||
|             onRefresh = onRefresh, | ||||
|             indicatorPadding = contentPadding, | ||||
|             indicator = { s, trigger -> | ||||
|                 SwipeRefreshIndicator( | ||||
|                     state = s, | ||||
|                     refreshTriggerDistance = trigger, | ||||
|                 ) | ||||
|             }, | ||||
|             bottomBar = { | ||||
|                 SharedMangaBottomActionMenu( | ||||
|                     selected = selected, | ||||
|                     onMultiBookmarkClicked = onMultiBookmarkClicked, | ||||
|                     onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, | ||||
|                     onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, | ||||
|                     onDownloadChapter = onDownloadChapter, | ||||
|                     onMultiDeleteClicked = onMultiDeleteClicked, | ||||
|                     fillFraction = 1f, | ||||
|                 ) | ||||
|             }, | ||||
|             snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|             floatingActionButton = { | ||||
|                 AnimatedVisibility( | ||||
|                     visible = chapters.any { !it.chapter.read } && selected.isEmpty(), | ||||
|                     enter = fadeIn(), | ||||
|                     exit = fadeOut(), | ||||
|                 ) { | ||||
|                     ExtendedFloatingActionButton( | ||||
|                         text = { | ||||
|                             val id = if (chapters.any { it.chapter.read }) { | ||||
|                                 R.string.action_resume | ||||
|                             } else { | ||||
|                                 R.string.action_start | ||||
|                             } | ||||
|                             Text(text = stringResource(id)) | ||||
|                         }, | ||||
|                         icon = { Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) }, | ||||
|                         onClick = onContinueReading, | ||||
|                         expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), | ||||
|                         modifier = Modifier | ||||
|                             .padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()), | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|         ) { contentPadding -> | ||||
|             val withNavBarContentPadding = contentPadding + | ||||
|                 WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() | ||||
|         ) { | ||||
|             VerticalFastScroller( | ||||
|                 listState = chapterListState, | ||||
|                 thumbAllowed = { scrollBehavior.state.offset == scrollBehavior.state.offsetLimit }, | ||||
|                 topContentPadding = withNavBarContentPadding.calculateTopPadding(), | ||||
|                 endContentPadding = withNavBarContentPadding.calculateEndPadding(LocalLayoutDirection.current), | ||||
|                 topContentPadding = topPadding, | ||||
|                 endContentPadding = noTopContentPadding.calculateEndPadding(layoutDirection), | ||||
|             ) { | ||||
|                 LazyColumn( | ||||
|                     modifier = Modifier.fillMaxHeight(), | ||||
|                     state = chapterListState, | ||||
|                     contentPadding = withNavBarContentPadding, | ||||
|                     contentPadding = noTopContentPadding, | ||||
|                 ) { | ||||
|                     item(contentType = "info_box") { | ||||
|                         MangaInfoBox( | ||||
|                             windowWidthSizeClass = WindowWidthSizeClass.Compact, | ||||
|                             appBarPadding = topPadding, | ||||
|                             title = state.manga.title, | ||||
|                             author = state.manga.author, | ||||
|                             artist = state.manga.artist, | ||||
|                             sourceName = remember { state.source.getNameForMangaInfo() }, | ||||
|                             isStubSource = remember { state.source is SourceManager.StubSource }, | ||||
|                             coverDataProvider = { state.manga }, | ||||
|                             status = state.manga.status, | ||||
|                             onCoverClick = onCoverClicked, | ||||
|                             doSearch = onSearch, | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
|                     item(contentType = "action_row") { | ||||
|                         MangaActionRow( | ||||
|                             favorite = state.manga.favorite, | ||||
|                             trackingCount = state.trackingCount, | ||||
|                             onAddToLibraryClicked = onAddToLibraryClicked, | ||||
|                             onWebViewClicked = onWebViewClicked, | ||||
|                             onTrackingClicked = onTrackingClicked, | ||||
|                             onEditCategory = onEditCategoryClicked, | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
|                     item(contentType = "desc") { | ||||
|                         ExpandableMangaDescription( | ||||
|                             defaultExpandState = state.isFromSource, | ||||
|                             description = state.manga.description, | ||||
|                             tagsProvider = { state.manga.genre }, | ||||
|                             onTagClicked = onTagClicked, | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
|                     item(contentType = "header") { | ||||
|                         ChapterHeader( | ||||
|                             chapterCount = chapters.size, | ||||
|                             isChapterFiltered = state.manga.chaptersFiltered(), | ||||
|                             onFilterButtonClicked = onFilterButtonClicked, | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
|                     sharedChapterItems( | ||||
|                         chapters = chapters, | ||||
|                         state = state, | ||||
| @@ -514,33 +517,40 @@ fun MangaScreenLargeImpl( | ||||
|             Row { | ||||
|                 val withNavBarContentPadding = contentPadding + | ||||
|                     WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() | ||||
|                 MangaInfoHeader( | ||||
|                 Column( | ||||
|                     modifier = Modifier | ||||
|                         .weight(1f) | ||||
|                         .verticalScroll(rememberScrollState()) | ||||
|                         .padding(bottom = withNavBarContentPadding.calculateBottomPadding()), | ||||
|                     windowWidthSizeClass = WindowWidthSizeClass.Expanded, | ||||
|                     appBarPadding = contentPadding.calculateTopPadding(), | ||||
|                     title = state.manga.title, | ||||
|                     author = state.manga.author, | ||||
|                     artist = state.manga.artist, | ||||
|                     description = state.manga.description, | ||||
|                     tagsProvider = { state.manga.genre }, | ||||
|                     sourceName = remember { state.source.getNameForMangaInfo() }, | ||||
|                     isStubSource = remember { state.source is SourceManager.StubSource }, | ||||
|                     coverDataProvider = { state.manga }, | ||||
|                     favorite = state.manga.favorite, | ||||
|                     status = state.manga.status, | ||||
|                     trackingCount = state.trackingCount, | ||||
|                     fromSource = state.isFromSource, | ||||
|                     onAddToLibraryClicked = onAddToLibraryClicked, | ||||
|                     onWebViewClicked = onWebViewClicked, | ||||
|                     onTrackingClicked = onTrackingClicked, | ||||
|                     onTagClicked = onTagClicked, | ||||
|                     onEditCategory = onEditCategoryClicked, | ||||
|                     onCoverClick = onCoverClicked, | ||||
|                     doSearch = onSearch, | ||||
|                 ) | ||||
|                 ) { | ||||
|                     MangaInfoBox( | ||||
|                         windowWidthSizeClass = windowWidthSizeClass, | ||||
|                         appBarPadding = contentPadding.calculateTopPadding(), | ||||
|                         title = state.manga.title, | ||||
|                         author = state.manga.author, | ||||
|                         artist = state.manga.artist, | ||||
|                         sourceName = remember { state.source.getNameForMangaInfo() }, | ||||
|                         isStubSource = remember { state.source is SourceManager.StubSource }, | ||||
|                         coverDataProvider = { state.manga }, | ||||
|                         status = state.manga.status, | ||||
|                         onCoverClick = onCoverClicked, | ||||
|                         doSearch = onSearch, | ||||
|                     ) | ||||
|                     MangaActionRow( | ||||
|                         favorite = state.manga.favorite, | ||||
|                         trackingCount = state.trackingCount, | ||||
|                         onAddToLibraryClicked = onAddToLibraryClicked, | ||||
|                         onWebViewClicked = onWebViewClicked, | ||||
|                         onTrackingClicked = onTrackingClicked, | ||||
|                         onEditCategory = onEditCategoryClicked, | ||||
|                     ) | ||||
|                     ExpandableMangaDescription( | ||||
|                         defaultExpandState = true, | ||||
|                         description = state.manga.description, | ||||
|                         tagsProvider = { state.manga.genre }, | ||||
|                         onTagClicked = onTagClicked, | ||||
|                     ) | ||||
|                 } | ||||
|  | ||||
|                 val chaptersWeight = if (windowWidthSizeClass == WindowWidthSizeClass.Medium) 1f else 2f | ||||
|                 VerticalFastScroller( | ||||
|   | ||||
| @@ -85,179 +85,185 @@ import kotlin.math.roundToInt | ||||
| private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) | ||||
|  | ||||
| @Composable | ||||
| fun MangaInfoHeader( | ||||
| fun MangaInfoBox( | ||||
|     modifier: Modifier = Modifier, | ||||
|     windowWidthSizeClass: WindowWidthSizeClass, | ||||
|     appBarPadding: Dp, | ||||
|     title: String, | ||||
|     author: String?, | ||||
|     artist: String?, | ||||
|     description: String?, | ||||
|     tagsProvider: () -> List<String>?, | ||||
|     sourceName: String, | ||||
|     isStubSource: Boolean, | ||||
|     coverDataProvider: () -> Manga, | ||||
|     favorite: Boolean, | ||||
|     status: Long, | ||||
|     trackingCount: Int, | ||||
|     fromSource: Boolean, | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
|     onWebViewClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: (() -> Unit)?, | ||||
|     onTagClicked: (String) -> Unit, | ||||
|     onEditCategory: (() -> Unit)?, | ||||
|     onCoverClick: () -> Unit, | ||||
|     doSearch: (query: String, global: Boolean) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     Column(modifier = modifier) { | ||||
|         Box { | ||||
|             // Backdrop | ||||
|             val backdropGradientColors = listOf( | ||||
|                 Color.Transparent, | ||||
|                 MaterialTheme.colorScheme.background, | ||||
|             ) | ||||
|             AsyncImage( | ||||
|                 model = coverDataProvider(), | ||||
|                 contentDescription = null, | ||||
|                 contentScale = ContentScale.Crop, | ||||
|                 modifier = Modifier | ||||
|                     .matchParentSize() | ||||
|                     .drawWithContent { | ||||
|                         drawContent() | ||||
|                         drawRect( | ||||
|                             brush = Brush.verticalGradient(colors = backdropGradientColors), | ||||
|                         ) | ||||
|                     } | ||||
|                     .alpha(.2f), | ||||
|             ) | ||||
|  | ||||
|             // Manga & source info | ||||
|             CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { | ||||
|                 if (windowWidthSizeClass == WindowWidthSizeClass.Compact) { | ||||
|                     MangaAndSourceTitlesSmall( | ||||
|                         appBarPadding = appBarPadding, | ||||
|                         coverDataProvider = coverDataProvider, | ||||
|                         onCoverClick = onCoverClick, | ||||
|                         title = title, | ||||
|                         context = context, | ||||
|                         doSearch = doSearch, | ||||
|                         author = author, | ||||
|                         artist = artist, | ||||
|                         status = status, | ||||
|                         sourceName = sourceName, | ||||
|                         isStubSource = isStubSource, | ||||
|                     ) | ||||
|                 } else { | ||||
|                     MangaAndSourceTitlesLarge( | ||||
|                         appBarPadding = appBarPadding, | ||||
|                         coverDataProvider = coverDataProvider, | ||||
|                         onCoverClick = onCoverClick, | ||||
|                         title = title, | ||||
|                         context = context, | ||||
|                         doSearch = doSearch, | ||||
|                         author = author, | ||||
|                         artist = artist, | ||||
|                         status = status, | ||||
|                         sourceName = sourceName, | ||||
|                         isStubSource = isStubSource, | ||||
|     Box(modifier = modifier) { | ||||
|         // Backdrop | ||||
|         val backdropGradientColors = listOf( | ||||
|             Color.Transparent, | ||||
|             MaterialTheme.colorScheme.background, | ||||
|         ) | ||||
|         AsyncImage( | ||||
|             model = coverDataProvider(), | ||||
|             contentDescription = null, | ||||
|             contentScale = ContentScale.Crop, | ||||
|             modifier = Modifier | ||||
|                 .matchParentSize() | ||||
|                 .drawWithContent { | ||||
|                     drawContent() | ||||
|                     drawRect( | ||||
|                         brush = Brush.verticalGradient(colors = backdropGradientColors), | ||||
|                     ) | ||||
|                 } | ||||
|                 .alpha(.2f), | ||||
|         ) | ||||
|  | ||||
|         // Manga & source info | ||||
|         CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { | ||||
|             if (windowWidthSizeClass == WindowWidthSizeClass.Compact) { | ||||
|                 MangaAndSourceTitlesSmall( | ||||
|                     appBarPadding = appBarPadding, | ||||
|                     coverDataProvider = coverDataProvider, | ||||
|                     onCoverClick = onCoverClick, | ||||
|                     title = title, | ||||
|                     context = LocalContext.current, | ||||
|                     doSearch = doSearch, | ||||
|                     author = author, | ||||
|                     artist = artist, | ||||
|                     status = status, | ||||
|                     sourceName = sourceName, | ||||
|                     isStubSource = isStubSource, | ||||
|                 ) | ||||
|             } else { | ||||
|                 MangaAndSourceTitlesLarge( | ||||
|                     appBarPadding = appBarPadding, | ||||
|                     coverDataProvider = coverDataProvider, | ||||
|                     onCoverClick = onCoverClick, | ||||
|                     title = title, | ||||
|                     context = LocalContext.current, | ||||
|                     doSearch = doSearch, | ||||
|                     author = author, | ||||
|                     artist = artist, | ||||
|                     status = status, | ||||
|                     sourceName = sourceName, | ||||
|                     isStubSource = isStubSource, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|         // Action buttons | ||||
|         Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { | ||||
|             val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) | ||||
| @Composable | ||||
| fun MangaActionRow( | ||||
|     modifier: Modifier = Modifier, | ||||
|     favorite: Boolean, | ||||
|     trackingCount: Int, | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
|     onWebViewClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: (() -> Unit)?, | ||||
|     onEditCategory: (() -> Unit)?, | ||||
| ) { | ||||
|     Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { | ||||
|         val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) | ||||
|         MangaActionButton( | ||||
|             title = if (favorite) { | ||||
|                 stringResource(R.string.in_library) | ||||
|             } else { | ||||
|                 stringResource(R.string.add_to_library) | ||||
|             }, | ||||
|             icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, | ||||
|             color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor, | ||||
|             onClick = onAddToLibraryClicked, | ||||
|             onLongClick = onEditCategory, | ||||
|         ) | ||||
|         if (onTrackingClicked != null) { | ||||
|             MangaActionButton( | ||||
|                 title = if (favorite) { | ||||
|                     stringResource(R.string.in_library) | ||||
|                 title = if (trackingCount == 0) { | ||||
|                     stringResource(R.string.manga_tracking_tab) | ||||
|                 } else { | ||||
|                     stringResource(R.string.add_to_library) | ||||
|                     quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount) | ||||
|                 }, | ||||
|                 icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, | ||||
|                 color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor, | ||||
|                 onClick = onAddToLibraryClicked, | ||||
|                 onLongClick = onEditCategory, | ||||
|                 icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done, | ||||
|                 color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, | ||||
|                 onClick = onTrackingClicked, | ||||
|             ) | ||||
|             if (onTrackingClicked != null) { | ||||
|                 MangaActionButton( | ||||
|                     title = if (trackingCount == 0) { | ||||
|                         stringResource(R.string.manga_tracking_tab) | ||||
|                     } else { | ||||
|                         quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount) | ||||
|                     }, | ||||
|                     icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done, | ||||
|                     color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary, | ||||
|                     onClick = onTrackingClicked, | ||||
|                 ) | ||||
|             } | ||||
|             if (onWebViewClicked != null) { | ||||
|                 MangaActionButton( | ||||
|                     title = stringResource(R.string.action_web_view), | ||||
|                     icon = Icons.Default.Public, | ||||
|                     color = defaultActionButtonColor, | ||||
|                     onClick = onWebViewClicked, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         if (onWebViewClicked != null) { | ||||
|             MangaActionButton( | ||||
|                 title = stringResource(R.string.action_web_view), | ||||
|                 icon = Icons.Default.Public, | ||||
|                 color = defaultActionButtonColor, | ||||
|                 onClick = onWebViewClicked, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|         // Expandable description-tags | ||||
|         Column { | ||||
|             val (expanded, onExpanded) = rememberSaveable { | ||||
|                 mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact) | ||||
|             } | ||||
|             val desc = | ||||
|                 description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) | ||||
|             val trimmedDescription = remember(desc) { | ||||
|                 desc | ||||
|                     .replace(whitespaceLineRegex, "\n") | ||||
|                     .trimEnd() | ||||
|             } | ||||
|             MangaSummary( | ||||
|                 expandedDescription = desc, | ||||
|                 shrunkDescription = trimmedDescription, | ||||
|                 expanded = expanded, | ||||
| @Composable | ||||
| fun ExpandableMangaDescription( | ||||
|     modifier: Modifier = Modifier, | ||||
|     defaultExpandState: Boolean, | ||||
|     description: String?, | ||||
|     tagsProvider: () -> List<String>?, | ||||
|     onTagClicked: (String) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     Column(modifier = modifier) { | ||||
|         val (expanded, onExpanded) = rememberSaveable { | ||||
|             mutableStateOf(defaultExpandState) | ||||
|         } | ||||
|         val desc = | ||||
|             description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder) | ||||
|         val trimmedDescription = remember(desc) { | ||||
|             desc | ||||
|                 .replace(whitespaceLineRegex, "\n") | ||||
|                 .trimEnd() | ||||
|         } | ||||
|         MangaSummary( | ||||
|             expandedDescription = desc, | ||||
|             shrunkDescription = trimmedDescription, | ||||
|             expanded = expanded, | ||||
|             modifier = Modifier | ||||
|                 .padding(top = 8.dp) | ||||
|                 .padding(horizontal = 16.dp) | ||||
|                 .clickableNoIndication( | ||||
|                     onLongClick = { context.copyToClipboard(desc, desc) }, | ||||
|                     onClick = { onExpanded(!expanded) }, | ||||
|                 ), | ||||
|         ) | ||||
|         val tags = tagsProvider() | ||||
|         if (!tags.isNullOrEmpty()) { | ||||
|             Box( | ||||
|                 modifier = Modifier | ||||
|                     .padding(top = 8.dp) | ||||
|                     .padding(horizontal = 16.dp) | ||||
|                     .clickableNoIndication( | ||||
|                         onLongClick = { context.copyToClipboard(desc, desc) }, | ||||
|                         onClick = { onExpanded(!expanded) }, | ||||
|                     ), | ||||
|             ) | ||||
|             val tags = tagsProvider() | ||||
|             if (!tags.isNullOrEmpty()) { | ||||
|                 Box( | ||||
|                     modifier = Modifier | ||||
|                         .padding(top = 8.dp) | ||||
|                         .padding(vertical = 12.dp) | ||||
|                         .animateContentSize(), | ||||
|                 ) { | ||||
|                     if (expanded) { | ||||
|                         FlowRow( | ||||
|                             modifier = Modifier.padding(horizontal = 16.dp), | ||||
|                             mainAxisSpacing = 4.dp, | ||||
|                             crossAxisSpacing = 8.dp, | ||||
|                         ) { | ||||
|                             tags.forEach { | ||||
|                                 TagsChip( | ||||
|                                     text = it, | ||||
|                                     onClick = { onTagClicked(it) }, | ||||
|                                 ) | ||||
|                             } | ||||
|                     .padding(vertical = 12.dp) | ||||
|                     .animateContentSize(), | ||||
|             ) { | ||||
|                 if (expanded) { | ||||
|                     FlowRow( | ||||
|                         modifier = Modifier.padding(horizontal = 16.dp), | ||||
|                         mainAxisSpacing = 4.dp, | ||||
|                         crossAxisSpacing = 8.dp, | ||||
|                     ) { | ||||
|                         tags.forEach { | ||||
|                             TagsChip( | ||||
|                                 text = it, | ||||
|                                 onClick = { onTagClicked(it) }, | ||||
|                             ) | ||||
|                         } | ||||
|                     } else { | ||||
|                         LazyRow( | ||||
|                             contentPadding = PaddingValues(horizontal = 16.dp), | ||||
|                             horizontalArrangement = Arrangement.spacedBy(4.dp), | ||||
|                         ) { | ||||
|                             items(items = tags) { | ||||
|                                 TagsChip( | ||||
|                                     text = it, | ||||
|                                     onClick = { onTagClicked(it) }, | ||||
|                                 ) | ||||
|                             } | ||||
|                     } | ||||
|                 } else { | ||||
|                     LazyRow( | ||||
|                         contentPadding = PaddingValues(horizontal = 16.dp), | ||||
|                         horizontalArrangement = Arrangement.spacedBy(4.dp), | ||||
|                     ) { | ||||
|                         items(items = tags) { | ||||
|                             TagsChip( | ||||
|                                 text = it, | ||||
|                                 onClick = { onTagClicked(it) }, | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|   | ||||
| @@ -1,141 +0,0 @@ | ||||
| package eu.kanade.presentation.manga.components | ||||
|  | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.material3.TopAppBarScrollBehavior | ||||
| import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.layout.Layout | ||||
| import androidx.compose.ui.layout.layoutId | ||||
| import androidx.compose.ui.layout.onSizeChanged | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.unit.Constraints | ||||
| import eu.kanade.domain.manga.model.Manga | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import kotlin.math.roundToInt | ||||
|  | ||||
| @Composable | ||||
| fun MangaTopAppBar( | ||||
|     modifier: Modifier = Modifier, | ||||
|     title: String, | ||||
|     author: String?, | ||||
|     artist: String?, | ||||
|     description: String?, | ||||
|     tagsProvider: () -> List<String>?, | ||||
|     coverDataProvider: () -> Manga, | ||||
|     sourceName: String, | ||||
|     isStubSource: Boolean, | ||||
|     favorite: Boolean, | ||||
|     status: Long, | ||||
|     trackingCount: Int, | ||||
|     chapterCount: Int?, | ||||
|     chapterFiltered: Boolean, | ||||
|     incognitoMode: Boolean, | ||||
|     downloadedOnlyMode: Boolean, | ||||
|     fromSource: Boolean, | ||||
|     onBackClicked: () -> Unit, | ||||
|     onCoverClick: () -> Unit, | ||||
|     onTagClicked: (String) -> Unit, | ||||
|     onAddToLibraryClicked: () -> Unit, | ||||
|     onWebViewClicked: (() -> Unit)?, | ||||
|     onTrackingClicked: (() -> Unit)?, | ||||
|     onFilterButtonClicked: () -> Unit, | ||||
|     onShareClicked: (() -> Unit)?, | ||||
|     onDownloadClicked: ((DownloadAction) -> Unit)?, | ||||
|     onEditCategoryClicked: (() -> Unit)?, | ||||
|     onMigrateClicked: (() -> Unit)?, | ||||
|     doGlobalSearch: (query: String, global: Boolean) -> Unit, | ||||
|     scrollBehavior: TopAppBarScrollBehavior?, | ||||
|     // For action mode | ||||
|     actionModeCounter: Int, | ||||
|     onSelectAll: () -> Unit, | ||||
|     onInvertSelection: () -> Unit, | ||||
|     onSmallAppBarHeightChanged: (Int) -> Unit, | ||||
| ) { | ||||
|     val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f } | ||||
|     val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() } | ||||
|  | ||||
|     Layout( | ||||
|         modifier = modifier, | ||||
|         content = { | ||||
|             val (smallHeightPx, onSmallHeightPxChanged) = remember { mutableStateOf(0) } | ||||
|             Column(modifier = Modifier.layoutId("mangaInfo")) { | ||||
|                 MangaInfoHeader( | ||||
|                     windowWidthSizeClass = WindowWidthSizeClass.Compact, | ||||
|                     appBarPadding = with(LocalDensity.current) { smallHeightPx.toDp() }, | ||||
|                     title = title, | ||||
|                     author = author, | ||||
|                     artist = artist, | ||||
|                     description = description, | ||||
|                     tagsProvider = tagsProvider, | ||||
|                     sourceName = sourceName, | ||||
|                     isStubSource = isStubSource, | ||||
|                     coverDataProvider = coverDataProvider, | ||||
|                     favorite = favorite, | ||||
|                     status = status, | ||||
|                     trackingCount = trackingCount, | ||||
|                     fromSource = fromSource, | ||||
|                     onAddToLibraryClicked = onAddToLibraryClicked, | ||||
|                     onWebViewClicked = onWebViewClicked, | ||||
|                     onTrackingClicked = onTrackingClicked, | ||||
|                     onTagClicked = onTagClicked, | ||||
|                     onEditCategory = onEditCategoryClicked, | ||||
|                     onCoverClick = onCoverClick, | ||||
|                     doSearch = doGlobalSearch, | ||||
|                 ) | ||||
|                 ChapterHeader( | ||||
|                     chapterCount = chapterCount, | ||||
|                     isChapterFiltered = chapterFiltered, | ||||
|                     onFilterButtonClicked = onFilterButtonClicked, | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             MangaSmallAppBar( | ||||
|                 modifier = Modifier | ||||
|                     .layoutId("topBar") | ||||
|                     .onSizeChanged { | ||||
|                         onSmallHeightPxChanged(it.height) | ||||
|                         onSmallAppBarHeightChanged(it.height) | ||||
|                     }, | ||||
|                 title = title, | ||||
|                 titleAlphaProvider = { if (actionModeCounter == 0) scrollPercentageProvider() else 1f }, | ||||
|                 incognitoMode = incognitoMode, | ||||
|                 downloadedOnlyMode = downloadedOnlyMode, | ||||
|                 onBackClicked = onBackClicked, | ||||
|                 onShareClicked = onShareClicked, | ||||
|                 onDownloadClicked = onDownloadClicked, | ||||
|                 onEditCategoryClicked = onEditCategoryClicked, | ||||
|                 onMigrateClicked = onMigrateClicked, | ||||
|                 actionModeCounter = actionModeCounter, | ||||
|                 onSelectAll = onSelectAll, | ||||
|                 onInvertSelection = onInvertSelection, | ||||
|             ) | ||||
|         }, | ||||
|     ) { measurables, constraints -> | ||||
|         val mangaInfoPlaceable = measurables | ||||
|             .first { it.layoutId == "mangaInfo" } | ||||
|             .measure(constraints.copy(maxHeight = Constraints.Infinity)) | ||||
|         val topBarPlaceable = measurables | ||||
|             .first { it.layoutId == "topBar" } | ||||
|             .measure(constraints) | ||||
|         val mangaInfoHeight = mangaInfoPlaceable.height | ||||
|         val topBarHeight = topBarPlaceable.height | ||||
|         val mangaInfoSansTopBarHeightPx = mangaInfoHeight - topBarHeight | ||||
|         val layoutHeight = topBarHeight + | ||||
|             (mangaInfoSansTopBarHeightPx * inverseScrollPercentageProvider()).roundToInt() | ||||
|  | ||||
|         layout(constraints.maxWidth, layoutHeight) { | ||||
|             val mangaInfoY = (-mangaInfoSansTopBarHeightPx * scrollPercentageProvider()).roundToInt() | ||||
|             mangaInfoPlaceable.place(0, mangaInfoY) | ||||
|             topBarPlaceable.place(0, 0) | ||||
|  | ||||
|             // Update offset limit | ||||
|             val offsetLimit = -mangaInfoSansTopBarHeightPx.toFloat() | ||||
|             if (scrollBehavior?.state?.offsetLimit != offsetLimit) { | ||||
|                 scrollBehavior?.state?.offsetLimit = offsetLimit | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user